From 3c0f1b49ffa06ad4536760b6a83b762188f5978f Mon Sep 17 00:00:00 2001 From: alpegon Date: Fri, 8 Nov 2019 09:47:21 +0100 Subject: [PATCH 01/41] WIP - Update SCAR to accept a function definition language --- scar/exceptions.py | 28 +++ scar/parser/cfgfile.py | 107 ++++---- scar/parser/cli.py | 246 ------------------- scar/parser/cli/__init__.py | 211 ++++++++++++++++ scar/parser/cli/parents.py | 116 +++++++++ scar/parser/cli/subparsers.py | 167 +++++++++++++ scar/parser/fdl.py | 42 ++++ scar/parser/fdl2.yaml | 40 +++ scar/parser/yaml.py | 57 ----- scar/providers/aws/__init__.py | 16 +- scar/providers/aws/apigateway.py | 89 +++---- scar/providers/aws/clients/__init__.py | 16 +- scar/providers/aws/clients/cloudwatchlogs.py | 9 - scar/providers/aws/controller.py | 209 +++++++++------- scar/providers/aws/functioncode.py | 35 +-- scar/providers/aws/iam.py | 3 + scar/providers/aws/lambdafunction.py | 197 +++++---------- scar/providers/aws/lambdalayers.py | 53 ++-- scar/providers/aws/response.py | 16 +- scar/providers/aws/s3.py | 3 +- scar/providers/aws/validators.py | 15 +- scar/scarcli.py | 25 +- scar/utils.py | 58 +++-- scar/version.py | 2 +- test/__init__.py | 0 test/functional/__init__.py | 0 test/functional/parser/__init__.py | 0 test/functional/parser/fdl.py | 52 ++++ test/functional/parser/fdl.yaml | 39 +++ video-process.yaml | 35 +++ 30 files changed, 1144 insertions(+), 742 deletions(-) delete mode 100644 scar/parser/cli.py create mode 100644 scar/parser/cli/__init__.py create mode 100644 scar/parser/cli/parents.py create mode 100644 scar/parser/cli/subparsers.py create mode 100644 scar/parser/fdl.py create mode 100644 scar/parser/fdl2.yaml delete mode 100644 scar/parser/yaml.py create mode 100644 test/__init__.py create mode 100644 test/functional/__init__.py create mode 100644 test/functional/parser/__init__.py create mode 100644 test/functional/parser/fdl.py create mode 100644 test/functional/parser/fdl.yaml create mode 100644 video-process.yaml diff --git a/scar/exceptions.py b/scar/exceptions.py index db465cb2..e9fd04a1 100644 --- a/scar/exceptions.py +++ b/scar/exceptions.py @@ -103,6 +103,15 @@ class YamlFileNotFoundError(ScarError): fmt = "Unable to find the yaml file '{file_path}'" +class FdlFileNotFoundError(ScarError): + """ + The configuration file does not exist + + :ivar file_path: Path of the file + """ + fmt = "Unable to find the configuration file '{file_path}'" + + class ValidatorError(ScarError): """ An error occurred when validating a parameter @@ -156,6 +165,23 @@ class GitHubTagNotFoundError(ScarError): fmt = "The tag '{tag}' was not found in the GitHub repository." +class StorageProviderNotSupportedError(ScarError): + """ + The storage provider parsed is not supported + + :ivar provider: Provider specified + """ + fmt = "The storage provider '{provider}' is not supported." + + +class AuthenticationVariableNotSupportedError(ScarError): + """ + The authentication variable parsed is not supported + + :ivar auth_var: Authentication variable specified + """ + fmt = "The authentication variable '{auth_var}' is not supported." + ################################################ # # LAMBDA EXCEPTIONS ## ################################################ @@ -253,6 +279,7 @@ class InvocationPayloadError(ScarError): "Check AWS Lambda invocation limits in : " "https://docs.aws.amazon.com/lambda/latest/dg/limits.html") + class NotExistentApiGatewayWarning(ScarError): """ The API with the id 'restApiId' was not found. @@ -261,6 +288,7 @@ class NotExistentApiGatewayWarning(ScarError): """ fmt = "The requested API '{restApiId}' does not exist." + ################################################ # # IAM EXCEPTIONS ## ################################################ diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index ced73ee2..0d250f10 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -21,27 +21,77 @@ _DEFAULT_CFG = { "scar": { - # Must be a tag or "latest" - "supervisor_version": "latest", - "config_version": "1.0.0" + "config_version": "1.0.5" }, "aws": { - "boto_profile": "default", - "region": "us-east-1", - "execution_mode": "lambda", - "iam": {"role": ""}, + "iam": {"boto_profile": "default", + "role": ""}, "lambda": { - "time": 300, + "boto_profile": "default", + "region": "us-east-1", + "execution_mode": "lambda", + "timeout": 300, "memory": 512, "description": "Automatically generated lambda function", - "timeout_threshold": 10, - "runtime": "python3.6", - "max_payload_size": 52428800, - "max_s3_payload_size": 262144000, - "layers": [] + "runtime": "python3.7", + "layers": [], + "invocation_type": "RequestResponse", + "asynchronous": False, + "log_type": "Tail", + "log_level": "INFO", + "environment": {"Variables": {}}, + "deployment": { + "max_payload_size": 52428800, + "max_s3_payload_size": 262144000 + }, + "container": { + "environment_variables": {}, + "timeout_threshold": 10, + "udocker": { + "bin": "/opt/udocker/bin/", + "dir": "/tmp/shared/udocker", + "exec": "/opt/udocker/udocker.py", + "lib": "/opt/udocker/lib/"} + }, + # Must be a Github tag or "latest" + "supervisor": { + "version": "latest", + 'layer_name' : "faas-supervisor", + 'license_info' : 'Apache 2.0' + } + }, + "api_gateway": { + "boto_profile": "default", + "region": "us-east-1", + "endpoint": "https://{api_id}.execute-api.{api_region}.amazonaws.com/scar/launch", + 'request_parameters': {"integration.request.header.X-Amz-Invocation-Type": + "method.request.header.X-Amz-Invocation-Type"}, + # ANY, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT + 'http_method': "ANY", + "method" : { + # NONE, AWS_IAM, CUSTOM, COGNITO_USER_POOLS + 'authorizationType': "NONE", + 'requestParameters': {'method.request.header.X-Amz-Invocation-Type' : False}, + }, + "integration": { + # 'HTTP'|'AWS'|'MOCK'|'HTTP_PROXY'|'AWS_PROXY' + 'type': "AWS_PROXY", + 'integrationHttpMethod' : "POST", + 'uri' : "arn:aws:apigateway:{api_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:{lambda_region}:{account_id}:function:{function_name}/invocations", + 'requestParameters' : {"integration.request.header.X-Amz-Invocation-Type": + "method.request.header.X-Amz-Invocation-Type"} + }, + 'path_part': "{proxy+}", + 'stage_name': "scar" + }, + "cloudwatch": { + "boto_profile": "default", + "region": "us-east-1", + "log_retention_policy_in_days": 30 }, - "cloudwatch": {"log_retention_policy_in_days": 30}, "batch": { + "boto_profile": "default", + "region": "us-east-1", "vcpus": 1, "memory": 1024, "enable_gpu": False, @@ -86,7 +136,7 @@ def _is_config_file_updated(self): if 'config_version' not in self.cfg_data['scar']: return False return StrUtils.compare_versions(self.cfg_data.get('scar', {}).get("config_version", ""), - _DEFAULT_CFG['scar']["config_version"]) <= 0 + _DEFAULT_CFG['scar']["config_version"]) >= 0 def get_properties(self): """Returns the configuration data of the configuration file.""" @@ -114,30 +164,3 @@ def _update_config_file(self): logger.info((f"New configuration file saved in '{self.config_file_path}'.\n" "Please fill your new configuration file with your account information.")) SysUtils.finish_scar_execution() - -# self._merge_files(self.cfg_data, _DEFAULT_CFG) -# self._delete_unused_data() -# with open(self.config_file_path, mode='w') as cfg_file: -# cfg_file.write(json.dumps(self.cfg_data, indent=2)) - -# def _add_missing_attributes(self): -# logger.info("Updating old scar config file '{0}'.\n".format(self.config_file_path)) -# FileUtils.copy_file(self.config_file_path, self.backup_file_path) -# logger.info("Old scar config file saved in '{0}'.\n".format(self.backup_file_path)) -# self._merge_files(self.cfg_data, _DEFAULT_CFG) -# self._delete_unused_data() -# with open(self.config_file_path, mode='w') as cfg_file: -# cfg_file.write(json.dumps(self.cfg_data, indent=2)) -# -# def _merge_files(self, cfg_data, default_data): -# for key, val in default_data.items(): -# if key not in cfg_data: -# cfg_data[key] = val -# elif isinstance(cfg_data[key], dict): -# self._merge_files(cfg_data[key], default_data[key]) -# -# def _delete_unused_data(self): -# if 'region' in self.cfg_data['aws']['lambda']: -# region = self.cfg_data['aws']['lambda'].pop('region', None) -# if region: -# self.cfg_data['aws']['region'] = region diff --git a/scar/parser/cli.py b/scar/parser/cli.py deleted file mode 100644 index c9f519b3..00000000 --- a/scar/parser/cli.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright (C) GRyCAP - I3M - UPV -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import sys -import scar.exceptions as excp -import scar.logger as logger -from scar.utils import DataTypesUtils -import scar.version as version - - -class CommandParser(object): - - def __init__(self, scar_cli): - self.scar_cli = scar_cli - self.create_parser() - self.create_parent_parsers() - self.add_subparsers() - - def create_parser(self): - self.parser = argparse.ArgumentParser(prog="scar", - description="Deploy containers in serverless architectures", - epilog="Run 'scar COMMAND --help' for more information on a command.") - self.parser.add_argument('--version', help='Show SCAR version.', dest="version", action="store_true", default=False) - - def create_parent_parsers(self): - self.create_function_definition_parser() - self.create_exec_parser() - self.create_output_parser() - self.create_profile_parser() - self.create_storage_parser() - - def create_function_definition_parser(self): - self.function_definition_parser = argparse.ArgumentParser(add_help=False) - self.function_definition_parser.add_argument("-d", "--description", help="Lambda function description.") - self.function_definition_parser.add_argument("-e", "--environment", action='append', help="Pass environment variable to the container (VAR=val). Can be defined multiple times.") - self.function_definition_parser.add_argument("-le", "--lambda-environment", action='append', help="Pass environment variable to the lambda function (VAR=val). Can be defined multiple times.") - self.function_definition_parser.add_argument("-m", "--memory", type=int, help="Lambda function memory in megabytes. Range from 128 to 3008 in increments of 64") - self.function_definition_parser.add_argument("-t", "--time", type=int, help="Lambda function maximum execution time in seconds. Max 900.") - self.function_definition_parser.add_argument("-tt", "--timeout-threshold", type=int, help="Extra time used to postprocess the data. This time is extracted from the total time of the lambda function.") - self.function_definition_parser.add_argument("-ll", "--log-level", help="Set the log level of the lambda function. Accepted values are: 'CRITICAL','ERROR','WARNING','INFO','DEBUG'", default="INFO") - self.function_definition_parser.add_argument("-l", "--layers", action='append', help="Pass layers ARNs to the lambda function. Can be defined multiple times.") - self.function_definition_parser.add_argument("-ib", "--input-bucket", help="Bucket name where the input files will be stored.") - self.function_definition_parser.add_argument("-ob", "--output-bucket", help="Bucket name where the output files are saved.") - self.function_definition_parser.add_argument("-em", "--execution-mode", help="Specifies the execution mode of the job. It can be 'lambda', 'lambda-batch' or 'batch'") - self.function_definition_parser.add_argument("-r", "--iam-role", help="IAM role used in the management of the functions") - self.function_definition_parser.add_argument("-sv", "--supervisor-version", help="FaaS Supervisor version. Can be a tag or 'latest'.") - # Batch (job definition) options - self.function_definition_parser.add_argument("-bm", "--batch-memory", help="Batch job memory in megabytes") - self.function_definition_parser.add_argument("-bc", "--batch-vcpus", help="Number of vCPUs reserved for the Batch container") - self.function_definition_parser.add_argument("-g", "--enable-gpu", help="Reserve one physical GPU for the Batch container (if it's available in the compute environment)", action="store_true") - - def create_exec_parser(self): - self.exec_parser = argparse.ArgumentParser(add_help=False) - self.exec_parser.add_argument("-a", "--asynchronous", help="Launch an asynchronous function.", action="store_true") - self.exec_parser.add_argument("-o", "--output-file", help="Save output as a file") - - def create_output_parser(self): - self.output_parser = argparse.ArgumentParser(add_help=False) - self.output_parser.add_argument("-j", "--json", help="Return data in JSON format", action="store_true") - self.output_parser.add_argument("-v", "--verbose", help="Show the complete aws output in json format", action="store_true") - - def create_profile_parser(self): - self.profile_parser = argparse.ArgumentParser(add_help=False) - self.profile_parser.add_argument("-pf", "--profile", help="AWS profile to use") - - def create_storage_parser(self): - self.storage_parser = argparse.ArgumentParser(add_help=False) - self.storage_parser.add_argument("-b", "--bucket", help="Bucket to use as storage", required=True) - self.storage_parser.add_argument("-p", "--path", help="Path of the file or folder", required=True) - - def add_subparsers(self): - self.subparsers = self.parser.add_subparsers(title='Commands') - self.add_init_parser() - self.add_invoke_parser() - self.add_run_parser() - self.add_update_parser() - self.add_rm_parser() - self.add_ls_parser() - self.add_log_parser() - self.add_put_parser() - self.add_get_parser() - - def add_init_parser(self): - parser_init = self.subparsers.add_parser('init', parents=[self.function_definition_parser, self.output_parser, self.profile_parser], help="Create lambda function") - # Set default function - parser_init.set_defaults(func=self.scar_cli.init) - # Lambda conf - group = parser_init.add_mutually_exclusive_group(required=True) - group.add_argument("-i", "--image", help="Container image id (i.e. centos:7)") - group.add_argument("-if", "--image-file", help="Container image file created with 'docker save' (i.e. centos.tar.gz)") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - parser_init.add_argument("-n", "--name", help="Lambda function name") - parser_init.add_argument("-s", "--init-script", help="Path to the input file passed to the function") - parser_init.add_argument("-ph", "--preheat", help="Preheats the function running it once and downloading the necessary container", action="store_true") - parser_init.add_argument("-ep", "--extra-payload", help="Folder containing files that are going to be added to the lambda function") - parser_init.add_argument("-db", "--deployment-bucket", help="Bucket where the deployment package is going to be uploaded.") - # API Gateway conf - parser_init.add_argument("-api", "--api-gateway-name", help="API Gateway name created to launch the lambda function") - - def add_invoke_parser(self): - parser_invoke = self.subparsers.add_parser('invoke', parents=[self.profile_parser, self.exec_parser], help="Call a lambda function using an HTTP request") - # Set default function - parser_invoke.set_defaults(func=self.scar_cli.invoke) - group = parser_invoke.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - parser_invoke.add_argument("-db", "--data-binary", help="File path of the HTTP data to POST.") - parser_invoke.add_argument("-jd", "--json-data", help="JSON Body to Post") - parser_invoke.add_argument("-p", "--parameters", help="In addition to passing the parameters in the URL, you can pass the parameters here (i.e. '{\"key1\": \"value1\", \"key2\": [\"value2\", \"value3\"]}').") - - def add_update_parser(self): - parser_update = self.subparsers.add_parser('update', parents=[self.function_definition_parser, self.output_parser, self.profile_parser], help="Update function properties") - parser_update.set_defaults(func=self.scar_cli.update) - group = parser_update.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-a", "--all", help="Update all lambda functions", action="store_true") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - - def add_run_parser(self): - parser_run = self.subparsers.add_parser('run', parents=[self.output_parser, self.profile_parser, self.exec_parser], help="Deploy function") - parser_run.set_defaults(func=self.scar_cli.run) - group = parser_run.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - parser_run.add_argument("-s", "--run-script", help="Path to the input file passed to the function") - parser_run.add_argument('c_args', nargs=argparse.REMAINDER, help="Arguments passed to the container.") - - def add_rm_parser(self): - parser_rm = self.subparsers.add_parser('rm', parents=[self.output_parser, self.profile_parser], help="Delete function") - parser_rm.set_defaults(func=self.scar_cli.rm) - group = parser_rm.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-a", "--all", help="Delete all lambda functions", action="store_true") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - - def add_log_parser(self): - parser_log = self.subparsers.add_parser('log', parents=[self.profile_parser], help="Show the logs for the lambda function") - parser_log.set_defaults(func=self.scar_cli.log) - group = parser_log.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - # CloudWatch args - parser_log.add_argument("-ls", "--log-stream-name", help="Return the output for the log stream specified.") - parser_log.add_argument("-ri", "--request-id", help="Return the output for the request id specified.") - - def add_ls_parser(self): - parser_ls = self.subparsers.add_parser('ls', parents=[self.output_parser, self.profile_parser], help="List lambda functions") - parser_ls.set_defaults(func=self.scar_cli.ls) - # S3 args - parser_ls.add_argument("-b", "--bucket", help="Show bucket files") - # Layer args - parser_ls.add_argument("-l", "--list-layers", help="Show lambda layers information", action="store_true") - - def add_put_parser(self): - parser_put = self.subparsers.add_parser('put', parents=[self.storage_parser, self.profile_parser], help="Upload file(s) to bucket") - parser_put.set_defaults(func=self.scar_cli.put) - - def add_get_parser(self): - parser_get = self.subparsers.add_parser('get', parents=[self.storage_parser, self.profile_parser], help="Download file(s) from bucket") - parser_get.set_defaults(func=self.scar_cli.get) - - @excp.exception(logger) - def parse_arguments(self): - """Command parsing and selection""" - try: - cmd_args = self.parser.parse_args() - if cmd_args.version: - print(f"SCAR {version.__version__}") - sys.exit(0) - - cmd_args = vars(cmd_args) - if 'func' not in cmd_args: - raise excp.MissingCommandError() - scar_args = self.parse_scar_args(cmd_args) - aws_args = self.parse_aws_args(cmd_args) - return DataTypesUtils.merge_dicts(scar_args, aws_args) - except AttributeError as aerr: - logger.error("Incorrect arguments: use scar -h to see the options available", - f"Error parsing arguments: {aerr}") - else: - raise - - def set_args(self, args, key, val): - if key and val: - args[key] = val - - def parse_aws_args(self, cmd_args): - aws_args = {} - other_args = [('profile', 'boto_profile'), 'region', 'execution_mode'] - self.set_args(aws_args, 'iam', self.parse_iam_args(cmd_args)) - self.set_args(aws_args, 'lambda', self.parse_lambda_args(cmd_args)) - self.set_args(aws_args, 'batch', self.parse_batch_args(cmd_args)) - self.set_args(aws_args, 'cloudwatch', self.parse_cloudwatchlogs_args(cmd_args)) - self.set_args(aws_args, 's3', self.parse_s3_args(cmd_args)) - self.set_args(aws_args, 'api_gateway', self.parse_api_gateway_args(cmd_args)) - aws_args.update(DataTypesUtils.parse_arg_list(other_args, cmd_args)) - return {'aws' : aws_args} - - def parse_scar_args(self, cmd_args): - scar_args = ['func', 'conf_file', 'json', - 'verbose', 'path', - 'preheat', 'execution_mode', - 'output_file', 'supervisor_version'] - return {'scar' : DataTypesUtils.parse_arg_list(scar_args, cmd_args)} - - def parse_lambda_args(self, cmd_args): - lambda_args = ['name', 'asynchronous', 'init_script', 'run_script', 'c_args', 'memory', 'time', - 'timeout_threshold', 'log_level', 'image', 'image_file', 'description', - 'lambda_role', 'extra_payload', ('environment', 'environment_variables'), - 'layers', 'lambda_environment', 'list_layers', 'all'] - return DataTypesUtils.parse_arg_list(lambda_args, cmd_args) - - def parse_batch_args(self, cmd_args): - batch_args = [('batch_vcpus', 'vcpus'), ('batch_memory', 'memory'), 'enable_gpu'] - return DataTypesUtils.parse_arg_list(batch_args, cmd_args) - - def parse_iam_args(self, cmd_args): - iam_args = [('iam_role', 'role')] - return DataTypesUtils.parse_arg_list(iam_args, cmd_args) - - def parse_cloudwatchlogs_args(self, cmd_args): - cw_log_args = ['log_stream_name', 'request_id'] - return DataTypesUtils.parse_arg_list(cw_log_args, cmd_args) - - def parse_api_gateway_args(self, cmd_args): - api_gtw_args = [('api_gateway_name', 'name'), 'parameters', 'data_binary', 'json_data'] - return DataTypesUtils.parse_arg_list(api_gtw_args, cmd_args) - - def parse_s3_args(self, cmd_args): - s3_args = ['deployment_bucket', - 'input_bucket', - 'output_bucket', - ('bucket', 'input_bucket')] - return DataTypesUtils.parse_arg_list(s3_args, cmd_args) diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py new file mode 100644 index 00000000..f049462f --- /dev/null +++ b/scar/parser/cli/__init__.py @@ -0,0 +1,211 @@ +# Copyright (C) GRyCAP - I3M - UPV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module with methods and classes in charge +of parsing the SCAR CLI commands.""" + +import argparse +import sys +from typing import Dict, List +from scar.parser.cli.subparsers import Subparsers +from scar.parser.cli.parents import * +from scar.utils import DataTypesUtils, StrUtils +import scar.exceptions as excp +import scar.logger as logger +import scar.version as version + + +def _parse_aws_args(cmd_args: Dict) -> Dict: + aws_args = {} + other_args = [('profile', 'boto_profile'), 'region', 'execution_mode'] + _set_args(aws_args, 'iam', _parse_iam_args(cmd_args)) + _set_args(aws_args, 'lambda', _parse_lambda_args(cmd_args)) + _set_args(aws_args, 'batch', _parse_batch_args(cmd_args)) + _set_args(aws_args, 'cloudwatch', _parse_cloudwatchlogs_args(cmd_args)) + _set_args(aws_args, 'api_gateway', _parse_api_gateway_args(cmd_args)) + aws_args.update(DataTypesUtils.parse_arg_list(other_args, cmd_args)) + storage = _parse_s3_args(aws_args, cmd_args) + result = {'functions': {'aws': [aws_args]}} + if storage: + result.update(storage) + return result + + +def _set_args(args: Dict, key: str, val: str) -> None: + if key and val: + args[key] = val + + +def _parse_scar_args(cmd_args: Dict) -> Dict: + scar_args = ['func', 'conf_file', 'json', + 'verbose', 'path', + 'preheat', 'execution_mode', + 'output_file', 'supervisor_version'] + return {'scar' : DataTypesUtils.parse_arg_list(scar_args, cmd_args)} + + +def _parse_iam_args(cmd_args: Dict) -> Dict: + iam_args = [('iam_role', 'role')] + return DataTypesUtils.parse_arg_list(iam_args, cmd_args) + + +def _parse_lambda_args(cmd_args: Dict) -> Dict: + lambda_arg_list = ['name', 'asynchronous', 'init_script', 'run_script', 'c_args', 'memory', + 'timeout', 'timeout_threshold', 'image', 'image_file', 'description', + 'lambda_role', 'extra_payload', ('environment', 'environment_variables'), + 'layers', 'lambda_environment', 'list_layers', 'all', 'log_level'] + lambda_args = DataTypesUtils.parse_arg_list(lambda_arg_list, cmd_args) + # Standardize log level if defined + if "log_level" in lambda_args: + lambda_args['log_level'] = lambda_args['log_level'].upper() + # Parse environment variables + lambda_args.update(_get_lambda_environment_variables(lambda_args)) + return lambda_args + + +def _get_lambda_environment_variables(lambda_args: Dict) -> None: + lambda_env_vars = {} + if "environment_variables" in lambda_args: + # These variables define the udocker container environment variables + lambda_env_vars['container'] = {'environment_variables' : {}} + for env_var in lambda_args["environment_variables"]: + key_val = env_var.split("=") + # Add an specific prefix to be able to find the container variables defined by the user + lambda_env_vars['container']['environment_variables'][f'{key_val[0]}'] = key_val[1] + del(lambda_args['environment_variables']) + if "extra_payload" in lambda_args: + lambda_env_vars['container']['extra_payload'] = f"/var/task" + if "init_script" in lambda_args: + lambda_env_vars['container']['init_script'] = f"/var/task/init_script.sh" + if "image" in lambda_args: + lambda_env_vars['container']['image'] = lambda_args.get('image') + del(lambda_args['image']) + + if "lambda_environment" in lambda_args: + # These variables define the lambda environment variables + lambda_env_vars['environment'] = {'Variables' : {}} + for env_var in lambda_args["lambda_environment"]: + key_val = env_var.split("=") + lambda_env_vars['environment']['Variables'][f'{key_val[0]}'] = key_val[1] + del(lambda_args['lambda_environment']) + return lambda_env_vars + + +def _parse_batch_args(cmd_args: Dict) -> Dict: + batch_args = [('batch_vcpus', 'vcpus'), ('batch_memory', 'memory'), 'enable_gpu'] + return DataTypesUtils.parse_arg_list(batch_args, cmd_args) + + +def _parse_cloudwatchlogs_args(cmd_args: Dict) -> Dict: + cw_log_args = ['log_stream_name', 'request_id'] + return DataTypesUtils.parse_arg_list(cw_log_args, cmd_args) + + +def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: + s3_arg_list = ['deployment_bucket', + 'input_bucket', + 'output_bucket', + ('bucket', 'input_bucket')] + + s3_args = DataTypesUtils.parse_arg_list(s3_arg_list, cmd_args) + storage = {} + if s3_args: + s3_id = StrUtils.get_random_uuid4_str() + if 'deployment_bucket' in s3_args: + aws_args['lambda']['deployment_bucket'] = s3_args['deployment_bucket'] + if 'input_bucket' in s3_args: + aws_args['lambda']['input'] = [{'storage_name' : s3_id, 'path': s3_args['input_bucket']}] + if 'output_bucket' in s3_args: + aws_args['lambda']['output'] = [{'storage_name' : s3_id, 'path': s3_args['output_bucket']}] + storage['storages'] = {'s3': [{'name': s3_id}]} + return storage + + +def _parse_api_gateway_args(cmd_args: Dict) -> Dict: + api_gtw_args = [('api_gateway_name', 'name'), 'parameters', 'data_binary', 'json_data'] + return DataTypesUtils.parse_arg_list(api_gtw_args, cmd_args) + + +def _create_main_parser(): + parser = argparse.ArgumentParser(prog="scar", + description=("Deploy containers " + "in serverless architectures"), + epilog=("Run 'scar COMMAND --help' " + "for more information on a command.")) + parser.add_argument('--version', + help='Show SCAR version.', + dest="version", + action="store_true", + default=False) + return parser + + +def _create_parent_parsers() -> Dict: + parsers = {} + parsers['function_definition_parser'] = create_function_definition_parser() + parsers['exec_parser'] = create_exec_parser() + parsers['output_parser'] = create_output_parser() + parsers['profile_parser'] = create_profile_parser() + parsers['storage_parser'] = create_storage_parser() + return parsers + + +class CommandParser(): + """Class to manage the SCAR CLI commands.""" + + def __init__(self, scar_cli): + self.scar_cli = scar_cli + self.parser = _create_main_parser() + self.parent_parsers = _create_parent_parsers() + self._add_subparsers() + + def _add_subparsers(self) -> None: + subparsers = Subparsers(self.parser.add_subparsers(title='Commands'), self.parent_parsers) + subparsers.add_subparser('init', self.scar_cli.init) + subparsers.add_subparser('invoke', self.scar_cli.invoke) + subparsers.add_subparser('run', self.scar_cli.run) + subparsers.add_subparser('update', self.scar_cli.update) + subparsers.add_subparser('rm', self.scar_cli.rm) + subparsers.add_subparser('ls', self.scar_cli.ls) + subparsers.add_subparser('log', self.scar_cli.log) + subparsers.add_subparser('put', self.scar_cli.put) + subparsers.add_subparser('get', self.scar_cli.get) + + @excp.exception(logger) + def parse_arguments(self) -> Dict: + """Command parsing and selection""" + try: + cmd_args = self.parser.parse_args() + if cmd_args.version: + print(f"SCAR {version.__version__}") + sys.exit(0) + + cmd_args = vars(cmd_args) + if 'func' not in cmd_args: + raise excp.MissingCommandError() + scar_args = _parse_scar_args(cmd_args) + aws_args = _parse_aws_args(cmd_args) + import pprint + print("--------------------------------------------SCAR---------------------------------------") + pprint.pprint(scar_args) + print("--------------------------------------------AWS---------------------------------------") + pprint.pprint(aws_args) + print("-----------------------------------------------------------------------------------------") + + return DataTypesUtils.merge_dicts_with_copy(scar_args, aws_args) + except AttributeError as aerr: + logger.error("Incorrect arguments: use scar -h to see the options available", + f"Error parsing arguments: {aerr}") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise diff --git a/scar/parser/cli/parents.py b/scar/parser/cli/parents.py new file mode 100644 index 00000000..3639f2f1 --- /dev/null +++ b/scar/parser/cli/parents.py @@ -0,0 +1,116 @@ +# Copyright (C) GRyCAP - I3M - UPV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + + +def create_function_definition_parser(): + function_definition_parser = argparse.ArgumentParser(add_help=False) + function_definition_parser.add_argument("-d", "--description", + help="Lambda function description.") + function_definition_parser.add_argument("-e", "--environment", + action='append', + help=("Pass environment variable to the container " + "(VAR=val). Can be defined multiple times.")) + function_definition_parser.add_argument("-le", "--lambda-environment", + action='append', + help=("Pass environment variable to the lambda " + "function (VAR=val). Can be defined multiple " + "times.")) + function_definition_parser.add_argument("-m", "--memory", + type=int, + help=("Lambda function memory in megabytes. " + "Range from 128 to 3008 in increments of 64")) + function_definition_parser.add_argument("-t", "--timeout", + type=int, + help=("Lambda function maximum execution " + "time in seconds. Max 900.")) + function_definition_parser.add_argument("-tt", "--timeout-threshold", + type=int, + help=("Extra time used to postprocess the data. " + "This time is extracted from the total " + "time of the lambda function.")) + function_definition_parser.add_argument("-ll", "--log-level", + help=("Set the log level of the lambda function. " + "Accepted values are: " + "'CRITICAL','ERROR','WARNING','INFO','DEBUG'")) + function_definition_parser.add_argument("-l", "--layers", + action='append', + help=("Pass layers ARNs to the lambda function. " + "Can be defined multiple times.")) + function_definition_parser.add_argument("-ib", "--input-bucket", + help=("Bucket name where the input files " + "will be stored.")) + function_definition_parser.add_argument("-ob", "--output-bucket", + help=("Bucket name where the output files are saved.")) + function_definition_parser.add_argument("-em", "--execution-mode", + help=("Specifies the execution mode of the job. " + "It can be 'lambda', 'lambda-batch' or 'batch'")) + function_definition_parser.add_argument("-r", "--iam-role", + help=("IAM role used in the management of " + "the functions")) + function_definition_parser.add_argument("-sv", "--supervisor-version", + help=("FaaS Supervisor version. " + "Can be a tag or 'latest'.")) + # Batch (job definition) options + function_definition_parser.add_argument("-bm", "--batch-memory", + help="Batch job memory in megabytes") + function_definition_parser.add_argument("-bc", "--batch-vcpus", + help=("Number of vCPUs reserved for the " + "Batch container")) + function_definition_parser.add_argument("-g", "--enable-gpu", + help=("Reserve one physical GPU for the Batch " + "container (if it's available in the " + "compute environment)"), + action="store_true") + return function_definition_parser + + +def create_exec_parser(): + exec_parser = argparse.ArgumentParser(add_help=False) + exec_parser.add_argument("-a", "--asynchronous", + help="Launch an asynchronous function.", + action="store_true") + exec_parser.add_argument("-o", "--output-file", + help="Save output as a file") + return exec_parser + + +def create_output_parser(): + output_parser = argparse.ArgumentParser(add_help=False) + output_parser.add_argument("-j", "--json", + help="Return data in JSON format", + action="store_true") + output_parser.add_argument("-v", "--verbose", + help="Show the complete aws output in json format", + action="store_true") + return output_parser + + +def create_profile_parser(): + profile_parser = argparse.ArgumentParser(add_help=False) + profile_parser.add_argument("-pf", "--profile", + help="AWS profile to use") + return profile_parser + + +def create_storage_parser(): + storage_parser = argparse.ArgumentParser(add_help=False) + storage_parser.add_argument("-b", "--bucket", + help="Bucket to use as storage", + required=True) + storage_parser.add_argument("-p", "--path", + help="Path of the file or folder", + required=True) + return storage_parser diff --git a/scar/parser/cli/subparsers.py b/scar/parser/cli/subparsers.py new file mode 100644 index 00000000..3d834e5d --- /dev/null +++ b/scar/parser/cli/subparsers.py @@ -0,0 +1,167 @@ +# Copyright (C) GRyCAP - I3M - UPV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse + +FUNCTION_DEFINITION = "function_definition_parser" +OUTPUT = "output_parser" +PROFILE = "profile_parser" +EXEC = "exec_parser" +STORAGE = "storage_parser" + +INIT_UPDATE_PARENTS = [PROFILE, FUNCTION_DEFINITION, OUTPUT] +INVOKE_PARENTS = [PROFILE, EXEC] +RUN_PARENTS = [PROFILE, EXEC, OUTPUT] +RM_LS_PARENTS = [PROFILE, OUTPUT] +LOG_PARENTS = [PROFILE] +PUT_GET_PARENTS = [PROFILE, STORAGE] + + +class Subparsers(): + + def __init__(self, subparser, parents): + self.subparser = subparser + self.parent_parsers = parents + + def _get_parents(self, parent_sublist): + return [self.parent_parsers.get(parent, "") for parent in parent_sublist] + + def add_subparser(self, name, scar_func): + getattr(self, f'_add_{name}_parser')(scar_func) + + def _add_init_parser(self, scar_func): + init = self.subparser.add_parser('init', + parents=self._get_parents(INIT_UPDATE_PARENTS), + help="Create lambda function") + # Set default function + init.set_defaults(func=scar_func) + # Lambda conf + group = init.add_mutually_exclusive_group(required=True) + group.add_argument("-i", "--image", + help="Container image id (i.e. centos:7)") + group.add_argument("-if", "--image-file", + help=("Container image file created with " + "'docker save' (i.e. centos.tar.gz)")) + group.add_argument("-f", "--conf-file", + help="Yaml file with the function configuration") + init.add_argument("-n", "--name", help="Lambda function name") + init.add_argument("-s", "--init-script", help=("Path to the input file " + "passed to the function")) + init.add_argument("-ph", "--preheat", + help=("Invokes the function once and downloads the container"), + action="store_true") + init.add_argument("-ep", "--extra-payload", + help=("Folder containing files that are going to be " + "added to the lambda function")) + init.add_argument("-db", "--deployment-bucket", + help="Bucket where the deployment package is going to be uploaded.") + # API Gateway conf + init.add_argument("-api", "--api-gateway-name", + help="API Gateway name created to launch the lambda function") + + def _add_invoke_parser(self, scar_func): + invoke = self.subparser.add_parser('invoke', + parents=self._get_parents(INVOKE_PARENTS), + help="Call a lambda function using an HTTP request") + # Set default function + invoke.set_defaults(func=scar_func) + group = invoke.add_mutually_exclusive_group(required=True) + group.add_argument("-n", "--name", + help="Lambda function name") + group.add_argument("-f", "--conf-file", + help="Yaml file with the function configuration") + invoke.add_argument("-db", "--data-binary", + help="File path of the HTTP data to POST.") + invoke.add_argument("-jd", "--json-data", + help="JSON Body to Post") + invoke.add_argument("-p", "--parameters", + help=("In addition to passing the parameters in the URL, " + "you can pass the parameters here (i.e. '{\"key1\": " + "\"value1\", \"key2\": [\"value2\", \"value3\"]}').")) + + def _add_update_parser(self, scar_func): + update = self.subparser.add_parser('update', + parents=self._get_parents(INIT_UPDATE_PARENTS), + help="Update function properties") + update.set_defaults(func=scar_func) + group = update.add_mutually_exclusive_group(required=True) + group.add_argument("-n", "--name", help="Lambda function name") + group.add_argument("-a", "--all", help="Update all lambda functions", action="store_true") + group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") + + def _add_run_parser(self, scar_func): + run = self.subparser.add_parser('run', + parents=self._get_parents(RUN_PARENTS), + help="Deploy function") + run.set_defaults(func=scar_func) + group = run.add_mutually_exclusive_group(required=True) + group.add_argument("-n", "--name", help="Lambda function name") + group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") + run.add_argument("-s", "--run-script", help="Path to the script passed to the function") + run.add_argument('c_args', + nargs=argparse.REMAINDER, + help="Arguments passed to the container.") + + def _add_rm_parser(self, scar_func): + rm = self.subparser.add_parser('rm', + parents=self._get_parents(RM_LS_PARENTS), + help="Delete function") + rm.set_defaults(func=scar_func) + group = rm.add_mutually_exclusive_group(required=True) + group.add_argument("-n", "--name", + help="Lambda function name") + group.add_argument("-a", "--all", + help="Delete all lambda functions", + action="store_true") + group.add_argument("-f", "--conf-file", + help="Yaml file with the function configuration") + + def _add_log_parser(self, scar_func): + log = self.subparser.add_parser('log', + parents=self._get_parents(LOG_PARENTS), + help="Show the logs for the lambda function") + log.set_defaults(func=scar_func) + group = log.add_mutually_exclusive_group(required=True) + group.add_argument("-n", "--name", + help="Lambda function name") + group.add_argument("-f", "--conf-file", + help="Yaml file with the function configuration") + # CloudWatch args + log.add_argument("-ls", "--log-stream-name", + help="Return the output for the log stream specified.") + log.add_argument("-ri", "--request-id", + help="Return the output for the request id specified.") + + def _add_ls_parser(self, scar_func): + ls = self.subparser.add_parser('ls', + parents=self._get_parents(RM_LS_PARENTS), + help="List lambda functions") + ls.set_defaults(func=scar_func) + # S3 args + ls.add_argument("-b", "--bucket", help="Show bucket files") + # Layer args + ls.add_argument("-l", "--list-layers", + help="Show lambda layers information", + action="store_true") + + def _add_put_parser(self, scar_func): + put = self.subparser.add_parser('put', + parents=self._get_parents(PUT_GET_PARENTS), + help="Upload file(s) to bucket") + put.set_defaults(func=scar_func) + + def _add_get_parser(self, scar_func): + get = self.subparser.add_parser('get', + parents=self._get_parents(PUT_GET_PARENTS), + help="Download file(s) from bucket") + get.set_defaults(func=scar_func) diff --git a/scar/parser/fdl.py b/scar/parser/fdl.py new file mode 100644 index 00000000..8445d062 --- /dev/null +++ b/scar/parser/fdl.py @@ -0,0 +1,42 @@ +# Copyright (C) GRyCAP - I3M - UPV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict +from scar.utils import DataTypesUtils + +FAAS_PROVIDERS = ["aws", "openfaas"] + +def merge_conf(conf: Dict, yaml: Dict) -> Dict: + result = yaml.copy() + # We have to merge the default config with all the defined functions + for provider in FAAS_PROVIDERS: + for index, function in enumerate(result.get('functions', {}).get(provider, {})): + result['functions'][provider][index] = \ + DataTypesUtils.merge_dicts_with_copy(conf.get(provider,{}), function) + result['scar'] = DataTypesUtils.merge_dicts_with_copy(result.get('scar', {}), + conf.get('scar', {})) + return result + +def merge_cmd_yaml(cmd: Dict, yaml: Dict) -> Dict: + result = yaml.copy() + # We merge the cli commands with all the defined functions + # CLI only allows define AWS parameters + for cli_cmd in cmd.get('functions', {}).get("aws",{}): + for index, function in enumerate(result.get('functions', {}).get("aws", {})): + result['functions']['aws'][index] = \ + DataTypesUtils.merge_dicts_with_copy(function, cli_cmd) + result['scar'] = DataTypesUtils.merge_dicts_with_copy(result.get('scar', {}), + cmd.get('scar', {})) + result['storages'] = DataTypesUtils.merge_dicts_with_copy(result.get('storages', {}), + cmd.get('storages', {})) + return result diff --git a/scar/parser/fdl2.yaml b/scar/parser/fdl2.yaml new file mode 100644 index 00000000..c9b9c60d --- /dev/null +++ b/scar/parser/fdl2.yaml @@ -0,0 +1,40 @@ +functions: +- aws: + - name: function1 + input: + - name: minio-local + path: my-bucket/test + - name: s3-bucket + path: s3-bucket/test1 + output: + - name: minio-local + path: my-bucket/test-output + files: + sufix: + - wav + - srt + - name: s3-bucket + path: s3-bucket/test1-output + files: + suffix: + - avi + - name: function2 + execution_mode: batch + input: + - name: minio-local + path: my-bucket2/test + output: + - name: minio-local + path: my-bucket2/test-output + files: + prefix: # Possible values: 'prefix', 'suffix' + - my_file + +storages: +- minio: # Possible values: 'minio', 's3', 'onedata' + - name: minio-local + auth: + user: muser # Possible values: 'user', 'pass', 'token', 'space', 'host' + pass: mpass +- s3: + - name: s3-bucket diff --git a/scar/parser/yaml.py b/scar/parser/yaml.py deleted file mode 100644 index bec1e665..00000000 --- a/scar/parser/yaml.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) GRyCAP - I3M - UPV -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import yaml -from scar.exceptions import YamlFileNotFoundError -from scar.utils import DataTypesUtils - - -class YamlParser(object): - - def __init__(self, scar_args): - file_path = scar_args['conf_file'] - if os.path.isfile(file_path): - with open(file_path) as cfg_file: - self.yaml_data = yaml.safe_load(cfg_file) - else: - raise YamlFileNotFoundError(file_path=file_path) - - def parse_arguments(self): - functions = [] - for function in self.yaml_data['functions']: - functions.append(self.parse_aws_function(function, self.yaml_data['functions'][function])) - return functions[0] - - def parse_aws_function(self, function_name, function_data): - aws_args = {} - scar_args = {} - # Get function name - aws_args['lambda'] = self.parse_lambda_args(function_data) - aws_args['lambda']['name'] = function_name - aws_services = ['iam', 'cloudwatch', 's3', 'api_gateway', 'batch'] - aws_args.update(DataTypesUtils.parse_arg_list(aws_services, function_data)) - other_args = [('profile', 'boto_profile'), 'region', 'execution_mode'] - aws_args.update(DataTypesUtils.parse_arg_list(other_args, function_data)) - scar_args.update(DataTypesUtils.parse_arg_list(['supervisor_version'], function_data)) - scar = {'scar': scar_args if scar_args else {}} - aws = {'aws': aws_args if aws_args else {}} - return DataTypesUtils.merge_dicts(scar, aws) - - def parse_lambda_args(self, cmd_args): - lambda_args = ['asynchronous', 'init_script', 'run_script', 'c_args', 'memory', 'time', - 'timeout_threshold', 'log_level', 'image', 'image_file', 'description', - 'lambda_role', 'extra_payload', ('environment', 'environment_variables'), - 'layers', 'lambda_environment'] - return DataTypesUtils.parse_arg_list(lambda_args, cmd_args) diff --git a/scar/providers/aws/__init__.py b/scar/providers/aws/__init__.py index 57c60c38..3c51d626 100644 --- a/scar/providers/aws/__init__.py +++ b/scar/providers/aws/__init__.py @@ -39,16 +39,18 @@ class GenericClient(): 'S3': S3Client, 'LAUNCHTEMPLATES': EC2Client} - def __init__(self, aws_properties: Dict): - self.aws = aws_properties - - def _get_client_args(self) -> Dict: - return {'client': {'region_name': self.aws.region}, - 'session': {'profile_name': self.aws.boto_profile}} + def __init__(self, client_properties: Dict): + self.properties = {} + region = client_properties.get('region') + if region: + self.properties['client'] = {'region_name': region} + session = client_properties.get('boto_profile') + if session: + self.properties['session'] = {'profile_name': session} @lazy_property def client(self): """Returns the required boto client based on the implementing class name.""" client_name = self.__class__.__name__.upper() - client = self._CLIENTS[client_name](**self._get_client_args()) + client = self._CLIENTS[client_name](self.properties) return client diff --git a/scar/providers/aws/apigateway.py b/scar/providers/aws/apigateway.py index 5ff8a213..2a876216 100644 --- a/scar/providers/aws/apigateway.py +++ b/scar/providers/aws/apigateway.py @@ -17,63 +17,34 @@ from scar.providers.aws import GenericClient import scar.logger as logger -# 'HTTP'|'AWS'|'MOCK'|'HTTP_PROXY'|'AWS_PROXY' -_DEFAULT_TYPE = "AWS_PROXY" -_DEFAULT_INTEGRATION_METHOD = "POST" -_DEFAULT_REQUEST_PARAMETERS = {"integration.request.header.X-Amz-Invocation-Type": - "method.request.header.X-Amz-Invocation-Type"} -# ANY, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT -_DEFAULT_HTTP_METHOD = "ANY" -# NONE, AWS_IAM, CUSTOM, COGNITO_USER_POOLS -_DEFAULT_AUTHORIZATION_TYPE = "NONE" -_DEFAULT_PATH_PART = "{proxy+}" -_DEFAULT_STAGE_NAME = "scar" - - class APIGateway(GenericClient): """Manage the calls to the ApiGateway client.""" - def __init__(self, aws_properties) -> None: - super().__init__(aws_properties) - # {0}: lambda function region, {1}: aws account id, {1}: lambda function name - self.lambda_uri = "arn:aws:lambda:{region}:{acc_id}:function:{lambdaf_name}/invocations" - # {0}: api_region, {1}: lambda_uri - self.uri = "arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_uri}" - # {0}: api_id, {1}: api_region - self.endpoint = "https://{api_id}.execute-api.{region}.amazonaws.com/scar/launch" - - def _get_uri(self) -> str: - lambda_uri_kwargs = {'region': self.aws.region, - 'acc_id': self.aws.account_id, - 'lambdaf_name': self.aws.lambdaf.name} - uri_kwargs = {'region': self.aws.region, - 'lambda_uri': self.lambda_uri.format(**lambda_uri_kwargs)} - return self.uri.format(**uri_kwargs) + def __init__(self, aws_properties: Dict): + super().__init__(aws_properties.get('api_gateway', {})) + self.aws = aws_properties + self.api = self.aws_properties.get('api_gateway', {}) - def _get_common_args(self, resource_info: Dict) -> Dict: - return {'restApiId' : self.aws.api_gateway.id, - 'resourceId' : resource_info.get('id', ''), - 'httpMethod' : _DEFAULT_HTTP_METHOD} + def _get_common_args(self) -> Dict: + return {'restApiId' : self.api.get('id', ''), + 'resourceId' : self.api.get('resource_id', ''), + 'httpMethod' : self.api.get('http_method', '')} - def _get_method_args(self, resource_info: Dict) -> Dict: - args = {'authorizationType' : _DEFAULT_AUTHORIZATION_TYPE, - 'requestParameters' : {'method.request.header.X-Amz-Invocation-Type' : False}} - method_args = self._get_common_args(resource_info) - method_args.update(args) - return method_args + def _get_method_args(self) -> Dict: + return self._get_common_args().update(self.api.get('method', {})) - def _get_integration_args(self, resource_info: Dict) -> Dict: - args = {'type' : _DEFAULT_TYPE, - 'integrationHttpMethod' : _DEFAULT_INTEGRATION_METHOD, - 'uri' : self._get_uri(), - 'requestParameters' : _DEFAULT_REQUEST_PARAMETERS} - integration_args = self._get_common_args(resource_info) - integration_args.update(args) - return integration_args + def _get_integration_args(self) -> Dict: + integration_args = self.api.get('integration', {}) + uri_args = {'api_region': self.api.get('region', ''), + 'lambda_region': self.aws.get('lambda', {}).get('region', ''), + 'account_id': self.aws.get('iam', {}).get('account_id', ''), + 'function_name': self.aws.get('lambda', {}).get('name', '')} + integration_args['uri'] = integration_args['uri'].format(**uri_args) + return self._get_common_args().update(integration_args) def _get_resource_id(self) -> str: res_id = "" - resources_info = self.client.get_resources(self.aws.api_gateway.id) + resources_info = self.client.get_resources(self.api.get('id','')) for resource in resources_info['items']: if resource['path'] == '/': res_id = resource['id'] @@ -81,22 +52,26 @@ def _get_resource_id(self) -> str: return res_id def _set_api_gateway_id(self, api_info: Dict) -> None: - self.aws.api_gateway.id = api_info.get('id', '') + self.api['id'] = api_info.get('id', '') + + def _set_resource_info_id(self, resource_info: Dict) -> None: + self.api['resource_id'] = resource_info.get('id', '') def _get_endpoint(self) -> str: - kwargs = {'api_id': self.aws.api_gateway.id, 'region': self.aws.region} - return self.endpoint.format(**kwargs) + endpoint_args = {'api_id': self.api.get('id',''), 'api_region': self.api.get('region','')} + return self.api.get('endpoint','').format(**endpoint_args) def create_api_gateway(self) -> None: """Creates an Api Gateway endpoint.""" - api_info = self.client.create_rest_api(self.aws.api_gateway.name) + api_info = self.client.create_rest_api(self.api.get('name','')) self._set_api_gateway_id(api_info) - resource_info = self.client.create_resource(self.aws.api_gateway.id, + resource_info = self.client.create_resource(self.api.get('id',''), self._get_resource_id(), - _DEFAULT_PATH_PART) - self.client.create_method(**self._get_method_args(resource_info)) - self.client.set_integration(**self._get_integration_args(resource_info)) - self.client.create_deployment(self.aws.api_gateway.id, _DEFAULT_STAGE_NAME) + self.api.get('path_part', '')) + self._set_resource_info_id(resource_info) + self.client.create_method(**self._get_method_args()) + self.client.set_integration(**self._get_integration_args()) + self.client.create_deployment(self.api.get('id',''), self.api.get('stage_name', '')) logger.info(f'API Gateway endpoint: {self._get_endpoint()}') def delete_api_gateway(self, api_gateway_id: str) -> None: diff --git a/scar/providers/aws/clients/__init__.py b/scar/providers/aws/clients/__init__.py index 38d84a03..b583cc2f 100644 --- a/scar/providers/aws/clients/__init__.py +++ b/scar/providers/aws/clients/__init__.py @@ -26,19 +26,9 @@ class BotoClient(): _READ_TIMEOUT = 360 _BOTO_CLIENT_NAME = '' - def __init__(self, client: Dict = None, session: Dict = None): - """ - Default client args: - 'client' : {'region_name' : self.aws.region} - Default session args: - 'session' : {'profile_name' : self.aws.boto_profile} - """ - self.client_args = {} - if client: - self.client_args = client - self.session_args = {} - if session: - self.session_args = session + def __init__(self, client_args: Dict): + self.client_args = client_args.get('client', {}) + self.session_args = client_args.get('session', {}) @lazy_property def client(self): diff --git a/scar/providers/aws/clients/cloudwatchlogs.py b/scar/providers/aws/clients/cloudwatchlogs.py index 1de5d293..4c7b56a0 100644 --- a/scar/providers/aws/clients/cloudwatchlogs.py +++ b/scar/providers/aws/clients/cloudwatchlogs.py @@ -31,15 +31,6 @@ class CloudWatchLogsClient(BotoClient): @exception(logger) def get_log_events(self, **kwargs: Dict) -> List: """Lists log events from the specified log group.""" -# logs = [] -# kwargs = {} -# response = self.client.filter_log_events(**kwargs) -# logs.append(response) -# while 'nextToken' in response and (response['nextToken']): -# kwargs['nextToken'] = response['nextToken'] -# response = self.client.filter_log_events(**kwargs) -# logs.append(response) -# return logs log_events = [] logs_info = self.client.filter_log_events(**kwargs) log_events.extend(logs_info.get('events', [])) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 38a2f6e5..1f89306b 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -21,7 +21,7 @@ from scar.providers.aws.cloudwatchlogs import CloudWatchLogs from scar.providers.aws.iam import IAM from scar.providers.aws.lambdafunction import Lambda -from scar.providers.aws.properties import AwsProperties, ScarProperties +# from scar.providers.aws.properties import AwsProperties, ScarProperties from scar.providers.aws.resourcegroups import ResourceGroups from scar.providers.aws.s3 import S3 from scar.providers.aws.validators import AWSValidator @@ -46,22 +46,94 @@ def _get_storage_provider_id(storage_provider: str, env_vars: Dict) -> str: return res +def _get_owner(function): + return _get_iam_client(function).get_user_name_or_id() + +############################################ +### BOTO CLIENTS ### +############################################ + + +def _get_iam_client(aws_properties: Dict): + return IAM(aws_properties) + + +def _get_lambda_client(aws_properties: Dict, supervisor_version: Dict): + return Lambda(aws_properties, supervisor_version) + + +def _get_api_gateway_client(aws_properties: Dict): + return APIGateway(aws_properties) + + +def _get_s3_client(aws_properties: Dict): + return S3(aws_properties) + +############################################ +### ADD EXTRA PROPERTIES ### +############################################ + + +def _add_extra_aws_properties(scar: Dict, aws_functions: Dict) -> None: + for function in aws_functions: + _add_tags(function) + _add_handler(function) + _add_account_id(function) + _add_output(scar, function) + _add_config_file_path(scar, function) + + +def _add_tags(function): + function['lambda']['tags'] = {"createdby": "scar", + "owner": _get_owner(function)} + + +def _add_account_id(function): + function['iam']['account_id'] = StrUtils.find_expression(function['iam']['role'], + _ACCOUNT_ID_REGEX) + + +def _add_handler(function): + function['lambda']['handler'] = f"{function.get('lambda', {}).get('name', '')}.lambda_handler" + + +def _add_output(scar_properties, function): + function['lambda']['output'] = response_parser.OutputType.PLAIN_TEXT + if scar_properties.get("json", False): + function['lambda']['output'] = response_parser.OutputType.JSON + # Override json ouput if both of them are defined + if scar_properties.get("verbose", False): + function['lambda']['output'] = response_parser.OutputType.VERBOSE + if scar_properties.get("output_file", False): + function['lambda']['output'] = response_parser.OutputType.BINARY + function['lambda']['output_file'] = scar_properties.get("output_file") + + +def _add_config_file_path(scar_properties, function): + if scar_properties.get("conf_file", False): + function['lambda']['config_path'] = os.path.dirname(scar_properties.get("conf_file")) + +############################################ +### AWS CONTROLLER ### +############################################ + + class AWS(Commands): """AWS controller. Used to manage all the AWS calls and functionalities.""" - @lazy_property - def aws_lambda(self): - """It's called 'aws_lambda' because 'lambda' - it's a restricted word in python.""" - aws_lambda = Lambda(self.aws_properties, - self.scar_properties.supervisor_version) - return aws_lambda +# @lazy_property +# def aws_lambda(self): +# """It's called 'aws_lambda' because 'lambda' +# it's a restricted word in python.""" +# aws_lambda = Lambda(self.aws_properties, +# self.scar.supervisor_version) +# return aws_lambda @lazy_property def batch(self): batch = Batch(self.aws_properties, - self.scar_properties.supervisor_version) + self.scar.supervisor_version) return batch @lazy_property @@ -74,34 +146,31 @@ def api_gateway(self): api_gateway = APIGateway(self.aws_properties) return api_gateway - @lazy_property - def aws_s3(self): - aws_s3 = S3(self.aws_properties) - return aws_s3 - @lazy_property def resource_groups(self): resource_groups = ResourceGroups(self.aws_properties) return resource_groups - @lazy_property - def iam(self): - iam = IAM(self.aws_properties) - return iam +############################################ +### AWS COMMANDS ### +############################################ @excp.exception(logger) - def init(self): - if self.aws_lambda.find_function(): - raise excp.FunctionExistsError(function_name=self.aws_properties.lambdaf.name) - # We have to create the gateway before creating the function - self._create_api_gateway() - self._create_lambda_function() - self._create_log_group() - self._create_s3_buckets() - # The api_gateway permissions are added after the function is created - self._add_api_gateway_permissions() - self._create_batch_environment() - self._preheat_function() + def init(self) -> None: + supervisor_version = self.scar.get('supervisor_version', 'latest') + for function in self.aws_functions: + lambda_client = _get_lambda_client(function, supervisor_version) + if lambda_client.find_function(): + raise excp.FunctionExistsError(function_name=function.get('lambda', {}).get('name', '')) + # We have to create the gateway before creating the function + self._create_api_gateway(function) + self._create_lambda_function(function, lambda_client) + self._create_log_group() + self._create_s3_buckets() + # The api_gateway permissions are added after the function is created + self._add_api_gateway_permissions() + self._create_batch_environment() + self._preheat_function() @excp.exception(logger) def invoke(self): @@ -111,7 +180,7 @@ def invoke(self): self.aws_properties.lambdaf.name, self.aws_properties.lambdaf.asynchronous, self.aws_properties.output, - getattr(self.scar_properties, "output_file", "")) + getattr(self.scar, "output_file", "")) @excp.exception(logger) def run(self): @@ -131,8 +200,8 @@ def update(self): @excp.exception(logger) def ls(self): - if hasattr(self.aws_properties, "s3"): - file_list = self.aws_s3.get_bucket_file_list() + if self.storages: + file_list = _get_s3_client(self.aws_functions).get_bucket_file_list() for file_info in file_list: print(file_info) else: @@ -165,39 +234,12 @@ def get(self): @AWSValidator.validate() @excp.exception(logger) - def parse_arguments(self, **kwargs): - self.raw_kwargs = kwargs - self.aws_properties = AwsProperties(kwargs.get('aws', {})) - self.scar_properties = ScarProperties(kwargs.get('scar', {})) - self.add_extra_aws_properties() - - def add_extra_aws_properties(self): - self._add_tags() - self._add_output() - self._add_account_id() - self._add_config_file_path() - - def _add_tags(self): - self.aws_properties.tags = {"createdby": "scar", "owner": self.iam.get_user_name_or_id()} - - def _add_output(self): - self.aws_properties.output = response_parser.OutputType.PLAIN_TEXT - if hasattr(self.scar_properties, "json") and self.scar_properties.json: - self.aws_properties.output = response_parser.OutputType.JSON - # Override json ouput if both of them are defined - if hasattr(self.scar_properties, "verbose") and self.scar_properties.verbose: - self.aws_properties.output = response_parser.OutputType.VERBOSE - if hasattr(self.scar_properties, "output_file") and self.scar_properties.output_file: - self.aws_properties.output = response_parser.OutputType.BINARY - self.aws_properties.output_file = self.scar_properties.output_file - - def _add_account_id(self): - self.aws_properties.account_id = StrUtils.find_expression(self.aws_properties.iam.role, - _ACCOUNT_ID_REGEX) - - def _add_config_file_path(self): - if hasattr(self.scar_properties, "conf_file") and self.scar_properties.conf_file: - self.aws_properties.config_path = os.path.dirname(self.scar_properties.conf_file) + def parse_arguments(self, merged_args: Dict) -> None: + self.raw_args = merged_args + self.aws_functions = merged_args.get('functions', {}).get('aws', {}) + self.storages = merged_args.get('storages', {}) + self.scar = merged_args.get('scar', {}) + _add_extra_aws_properties(self.scar, self.aws_functions) def _get_all_functions(self): arn_list = self.resource_groups.get_resource_arn_list(self.iam.get_user_name_or_id()) @@ -211,14 +253,6 @@ def _get_batch_logs(self) -> str: logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - @excp.exception(logger) - def _create_lambda_function(self): - response = self.aws_lambda.create_function() - acc_key = self.aws_lambda.client.get_access_key() - response_parser.parse_lambda_function_creation_response(response, - self.aws_properties.lambdaf.name, - acc_key, - self.aws_properties.output) @excp.exception(logger) def _create_log_group(self): @@ -237,10 +271,6 @@ def _create_s3_buckets(self): if hasattr(self.aws_properties.s3, "output_bucket"): self.aws_s3.create_output_bucket() - def _create_api_gateway(self): - if hasattr(self.aws_properties, "api_gateway"): - self.api_gateway.create_api_gateway() - def _add_api_gateway_permissions(self): if hasattr(self.aws_properties, "api_gateway"): self.aws_lambda.add_invocation_permission_from_api_gateway() @@ -252,7 +282,7 @@ def _create_batch_environment(self): def _preheat_function(self): # If preheat is activated, the function is launched at the init step - if hasattr(self.scar_properties, "preheat"): + if hasattr(self.scar, "preheat"): self.aws_lambda.preheat_function() def _process_input_bucket_calls(self): @@ -268,7 +298,7 @@ def _process_input_bucket_calls(self): self.aws_lambda.process_asynchronous_lambda_invocations(s3_event_list) def upload_file_or_folder_to_s3(self): - path_to_upload = self.scar_properties.path + path_to_upload = self.scar.path self.aws_s3.create_input_bucket() files = [path_to_upload] if os.path.isdir(path_to_upload): @@ -279,8 +309,8 @@ def upload_file_or_folder_to_s3(self): def _get_download_file_path(self, file_key=None): file_path = file_key - if hasattr(self.scar_properties, "path") and self.scar_properties.path: - file_path = FileUtils.join_paths(self.scar_properties.path, file_path) + if hasattr(self.scar, "path") and self.scar.path: + file_path = FileUtils.join_paths(self.scar.path, file_path) return file_path def download_file_or_folder_from_s3(self): @@ -313,6 +343,21 @@ def _update_local_function_properties(self, function_info): else: self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) +############################################################################# +### Methods to create AWS resources ### +############################################################################# + + def _create_api_gateway(self, function: Dict): + if function.get("api_gateway", {}).get('name', False): + _get_api_gateway_client(function).create_api_gateway() + + @excp.exception(logger) + def _create_lambda_function(self, function: Dict, lambda_client: Lambda) -> None: + response = lambda_client.create_function(function) + response_parser.parse_lambda_function_creation_response(response, + function, + lambda_client.get_access_key()) + ############################################################################# ### Methods to delete AWS resources ### ############################################################################# diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 34697415..11c9bad8 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -35,42 +35,43 @@ def udocker(self): udocker = Udocker(self.aws, self.scar_tmp_function_folder_path, self._supervisor_zip_path) return udocker - def __init__(self, aws_properties, supervisor_version): + def __init__(self, aws_properties): self.aws = aws_properties - self.supervisor_version = supervisor_version - self.scar_tmp_function_folder = FileUtils.create_tmp_dir() - self.scar_tmp_function_folder_path = self.scar_tmp_function_folder.name - self._supervisor_zip_path = FileUtils.join_paths(self.aws.lambdaf.tmp_folder_path, 'faas.zip') - +# self.scar_tmp_function_folder = FileUtils.create_tmp_dir() +# self.scar_tmp_function_folder_path = self.scar_tmp_function_folder.name +# self._supervisor_zip_path = FileUtils.join_paths(self.aws.lambdaf.tmp_folder_path, 'faas.zip') self.package_args = {} @exception(logger) - def create_zip(self): + def create_zip(self, tmp_folder): """Creates the lambda function deployment package.""" - self._download_faas_supervisor_zip() - self._extract_handler_code() + zip_path = FileUtils.join_paths(tmp_folder, 'faas.zip') + self._download_faas_supervisor_zip(zip_path) + self._extract_handler_code(tmp_folder, zip_path) self._manage_udocker_images() self._add_init_script() self._add_extra_payload() self._zip_scar_folder() self._check_code_size() + return zip_path - def _download_faas_supervisor_zip(self) -> None: + def _download_faas_supervisor_zip(self, zip_path) -> None: supervisor_zip_url = GitHubUtils.get_source_code_url( GITHUB_USER, GITHUB_SUPERVISOR_PROJECT, - self.supervisor_version) - with open(self._supervisor_zip_path, "wb") as thezip: + self.aws.get('lambda').get('supervisor').get('version')) + with open(zip_path, "wb") as thezip: thezip.write(get_file(supervisor_zip_url)) - def _extract_handler_code(self) -> None: - function_handler_dest = FileUtils.join_paths(self.scar_tmp_function_folder_path, f"{self.aws.lambdaf.name}.py") + def _extract_handler_code(self, tmp_folder, zip_path) -> None: + function_handler_dest = FileUtils.join_paths(tmp_folder, + f"{self.aws.get('lambda').get('name')}.py") file_path = "" - with ZipFile(self._supervisor_zip_path) as thezip: + with ZipFile(zip_path) as thezip: for file in thezip.namelist(): if file.endswith("function_handler.py"): - file_path = FileUtils.join_paths(self.aws.lambdaf.tmp_folder_path, file) - thezip.extract(file, self.aws.lambdaf.tmp_folder_path) + file_path = FileUtils.join_paths(tmp_folder, file) + thezip.extract(file, tmp_folder) break FileUtils.copy_file(file_path, function_handler_dest) diff --git a/scar/providers/aws/iam.py b/scar/providers/aws/iam.py index b00713d6..c37d3f3d 100644 --- a/scar/providers/aws/iam.py +++ b/scar/providers/aws/iam.py @@ -17,6 +17,9 @@ class IAM(GenericClient): + def __init__(self, aws_properties) -> None: + super().__init__(aws_properties.get('iam')) + def get_user_name_or_id(self): user = self.client.get_user_info() if user: diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 59cfe936..2a50dbae 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -14,180 +14,95 @@ import base64 import json -import random from multiprocessing.pool import ThreadPool from botocore.exceptions import ClientError from scar.providers.aws import GenericClient from scar.providers.aws.functioncode import FunctionPackager from scar.providers.aws.lambdalayers import LambdaLayers +from scar.providers.aws.clients.lambdafunction import LambdaClient from scar.providers.aws.s3 import S3 from scar.providers.aws.validators import AWSValidator import scar.exceptions as excp import scar.http.request as request import scar.logger as logger import scar.providers.aws.response as response_parser -from scar.utils import lazy_property, DataTypesUtils, FileUtils, StrUtils +from scar.utils import DataTypesUtils, FileUtils, StrUtils +from typing import Dict MAX_CONCURRENT_INVOCATIONS = 500 +ASYNCHRONOUS_CALL = {"invocation_type": "Event", + "log_type": "None", + "asynchronous": "True"} +REQUEST_RESPONSE_CALL = {"invocation_type": "RequestResponse", + "log_type": "Tail", + "asynchronous": "False"} + + +def _get_layers_client(client: LambdaClient, supervisor_version: str) -> LambdaLayers: + return LambdaLayers(client, supervisor_version) + + +def _get_s3_client(aws_properties: Dict) -> S3: + return S3(aws_properties) class Lambda(GenericClient): - @lazy_property - def layers(self): - layers = LambdaLayers(self.client, self.supervisor_version) - return layers - - @lazy_property - def s3(self): - s3 = S3(self.aws) - return s3 - - def __init__(self, aws_properties, supervisor_version): - super().__init__(aws_properties) - self.supervisor_version = supervisor_version - self._initialize_properties(aws_properties) - - def _initialize_properties(self, aws_properties): - self.aws.lambdaf.environment = {'Variables': {}} - self.aws.lambdaf.invocation_type = "RequestResponse" - self.aws.lambdaf.log_type = "Tail" - self.aws.lambdaf.layers = [] - self.aws.lambdaf.tmp_folder = FileUtils.create_tmp_dir() - self.aws.lambdaf.tmp_folder_path = self.aws.lambdaf.tmp_folder.name - self.aws.lambdaf.zip_file_path = FileUtils.join_paths(self.aws.lambdaf.tmp_folder_path, 'function.zip') - if hasattr(self.aws.lambdaf, "name"): - self.aws.lambdaf.handler = "{0}.lambda_handler".format(self.aws.lambdaf.name) - if not hasattr(self.aws.lambdaf, "asynchronous"): - self.aws.lambdaf.asynchronous = False - self._set_default_call_parameters() - - def _set_default_call_parameters(self): - self.asynchronous_call_parameters = {"invocation_type": "Event", - "log_type": "None", - "asynchronous": "True"} - self.request_response_call_parameters = {"invocation_type": "RequestResponse", - "log_type": "Tail", - "asynchronous": "False"} + def __init__(self, aws_properties: Dict) -> None: + super().__init__(aws_properties.get('lambda')) + self.aws = aws_properties + self.function = aws_properties.get('lambda', {}) + self.tmp_folder = FileUtils.create_tmp_dir() + self.zip_file_path = FileUtils.join_paths(self.tmp_folder.name, 'function.zip') def _get_creations_args(self): - return {'FunctionName': self.aws.lambdaf.name, - 'Runtime': self.aws.lambdaf.runtime, - 'Role': self.aws.iam.role, - 'Handler': self.aws.lambdaf.handler, - 'Code': self.aws.lambdaf.code, - 'Environment': self.aws.lambdaf.environment, - 'Description': self.aws.lambdaf.description, - 'Timeout': self.aws.lambdaf.time, - 'MemorySize': self.aws.lambdaf.memory, - 'Tags': self.aws.tags, - 'Layers': self.aws.lambdaf.layers} + return {'FunctionName': self.function.get('name'), + 'Runtime': self.function.get('runtime'), + 'Role': self.aws.get('iam').get('role'), + 'Handler': self.function.get('handler'), + 'Code': self._get_function_code(), + 'Environment': self.function.get('environment'), + 'Description': self.function.get('description'), + 'Timeout': self.function.get('time'), + 'MemorySize': self.function.get('memory'), + 'Tags': self.function.get('tags'), + 'Layers': self.function.get('layers')} def is_asynchronous(self): - return self.aws.lambdaf.asynchronous + return self.function.get('asynchronous', False) + + def get_access_key(self) -> str: + """Returns the access key belonging to the boto_profile used.""" + return self.client.get_access_key() @excp.exception(logger) def create_function(self): self._manage_supervisor_layer() - self._set_environment_variables() - self._set_function_code() creation_args = self._get_creations_args() response = self.client.create_function(**creation_args) if response and "FunctionArn" in response: - self.aws.lambdaf.arn = response.get('FunctionArn', "") + self.function['arn'] = response.get('FunctionArn', "") return response def _manage_supervisor_layer(self): - self.layers.check_faas_supervisor_layer() - self.aws.lambdaf.layers.append(self.layers.get_latest_supervisor_layer_arn()) - - def _add_lambda_environment_variable(self, key, value): - if key and value: - self.aws.lambdaf.environment['Variables'][key] = value - - def _add_custom_environment_variables(self, env_vars, prefix=''): - if type(env_vars) is dict: - for key, val in env_vars.items(): - # Add an specific prefix to be able to find the variables defined by the user - self._add_lambda_environment_variable('{0}{1}'.format(prefix, key), val) - else: - for env_var in env_vars: - key_val = env_var.split("=") - # Add an specific prefix to be able to find the variables defined by the user - self._add_lambda_environment_variable('{0}{1}'.format(prefix, key_val[0]), key_val[1]) - - def _set_environment_variables(self): - # Add required variables - self._set_required_environment_variables() - # Add explicitly user defined variables - if hasattr(self.aws.lambdaf, "environment_variables"): - self._add_custom_environment_variables(self.aws.lambdaf.environment_variables, prefix='CONT_VAR_') - # Add explicitly user defined variables - if hasattr(self.aws.lambdaf, "lambda_environment"): - self._add_custom_environment_variables(self.aws.lambdaf.lambda_environment) - - def _set_required_environment_variables(self): - self._add_lambda_environment_variable('SUPERVISOR_TYPE', 'LAMBDA') - self._add_lambda_environment_variable('TIMEOUT_THRESHOLD', str(self.aws.lambdaf.timeout_threshold)) - self._add_lambda_environment_variable('LOG_LEVEL', self.aws.lambdaf.log_level) - self._add_udocker_variables() - self._add_execution_mode() - self._add_s3_environment_vars() - if hasattr(self.aws.lambdaf, "image"): - self._add_lambda_environment_variable('IMAGE_ID', self.aws.lambdaf.image) - if hasattr(self.aws, "api_gateway"): - self._add_lambda_environment_variable('API_GATEWAY_ID', self.aws.api_gateway.id) - - def _add_udocker_variables(self): - self._add_lambda_environment_variable('UDOCKER_EXEC', "/opt/udocker/udocker.py") - self._add_lambda_environment_variable('UDOCKER_DIR', "/tmp/shared/udocker") - self._add_lambda_environment_variable('UDOCKER_LIB', "/opt/udocker/lib/") - self._add_lambda_environment_variable('UDOCKER_BIN', "/opt/udocker/bin/") - - def _add_execution_mode(self): - self._add_lambda_environment_variable('EXECUTION_MODE', self.aws.execution_mode) - # if (self.aws.execution_mode == 'lambda-batch' or self.aws.execution_mode == 'batch'): - # self._add_lambda_environment_variable('BATCH_SUPERVISOR_IMG', self.aws.batch.supervisor_image) - - def _add_s3_environment_vars(self): - if hasattr(self.aws, "s3"): - provider_id = random.randint(1, 1000001) - - if hasattr(self.aws.s3, "input_bucket"): - self._add_lambda_environment_variable( - f'STORAGE_PATH_INPUT_{provider_id}', - self.aws.s3.storage_path_input - ) - - if hasattr(self.aws.s3, "output_bucket"): - self._add_lambda_environment_variable( - f'STORAGE_PATH_OUTPUT_{provider_id}', - self.aws.s3.storage_path_output - ) - else: - self._add_lambda_environment_variable( - f'STORAGE_PATH_OUTPUT_{provider_id}', - self.aws.s3.storage_path_input - ) - self._add_lambda_environment_variable( - f'STORAGE_AUTH_S3_USER_{provider_id}', - 'scar' - ) + layers_client = LambdaLayers(self.function, self.client) + layers_client.check_faas_supervisor_layer() + self.function.get('layers', []).append(layers_client.get_latest_supervisor_layer_arn()) @excp.exception(logger) - def _set_function_code(self): + def _get_function_code(self): # Zip all the files and folders needed - FunctionPackager(self.aws, self.supervisor_version).create_zip() - if hasattr(self.aws, "s3") and hasattr(self.aws.s3, 'deployment_bucket'): - self._upload_to_S3() - self.aws.lambdaf.code = {"S3Bucket": self.aws.s3.deployment_bucket, "S3Key": self.aws.s3.file_key} + code = {} + FunctionPackager(self.aws).create_zip(self.tmp_folder.name) + if self.function.get('deployment_bucket', False): + file_key = f"lambda/{self.function.get('name')}.zip" + self._get_s3_client().upload_file(file_path=self.zip_file_path, + file_key=file_key) + code = {"S3Bucket": self.function.get('deployment_bucket'), + "S3Key": file_key} else: - self.aws.lambdaf.code = {"ZipFile": FileUtils.read_file(self.aws.lambdaf.zip_file_path, mode="rb")} - - def _upload_to_S3(self): - self.aws.s3.input_bucket = self.aws.s3.deployment_bucket - self.aws.s3.file_key = 'lambda/{0}.zip'.format(self.aws.lambdaf.name) - self.s3.upload_file(file_path=self.aws.lambdaf.zip_file_path, file_key=self.aws.s3.file_key) + code = {"ZipFile": FileUtils.read_file(self.zip_file_path, mode="rb")} + return code def delete_function(self, function_name): return self.client.delete_function(function_name) @@ -242,7 +157,7 @@ def launch_lambda_instance(self): def _get_invocation_payload(self): # Default payload payload = self.aws.lambdaf.payload if hasattr(self.aws.lambdaf, 'payload') else {} - if not payload: + if not payload: # Check for defined run script if hasattr(self.aws.lambdaf, "run_script"): script_path = self.aws.lambdaf.run_script @@ -329,7 +244,7 @@ def get_function_info(self, function_name_or_arn=None): def find_function(self, function_name_or_arn=None): try: # If this call works the function exists - name_arn = function_name_or_arn if function_name_or_arn else self.aws.lambdaf.name + name_arn = function_name_or_arn if function_name_or_arn else self.function.get('name', '') self.get_function_info(name_arn) return True except ClientError as ce: diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index c3f9609a..95c3fcfe 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -16,12 +16,13 @@ import io import shutil from typing import Dict + import zipfile -from tabulate import tabulate import scar.http.request as request import scar.logger as logger from scar.utils import lazy_property, FileUtils, GitHubUtils, StrUtils, \ GITHUB_USER, GITHUB_SUPERVISOR_PROJECT +from scar.providers.aws.clients.lambdafunction import LambdaClient def _create_tmp_folders() -> None: @@ -31,7 +32,8 @@ def _create_tmp_folders() -> None: def _download_supervisor(supervisor_version: str, tmp_zip_path: str) -> str: - supervisor_zip_url = GitHubUtils.get_source_code_url(GITHUB_USER, GITHUB_SUPERVISOR_PROJECT, + supervisor_zip_url = GitHubUtils.get_source_code_url(GITHUB_USER, + GITHUB_SUPERVISOR_PROJECT, supervisor_version) supervisor_zip = request.get_file(supervisor_zip_url) with zipfile.ZipFile(io.BytesIO(supervisor_zip)) as thezip: @@ -92,7 +94,7 @@ def delete(self, **kwargs: Dict) -> Dict: layer_args['VersionNumber'] = version_info.get('Version', -1) return self.lambda_client.delete_layer_version(**layer_args) - def get_latest_layer_info(self, layer_name: str) -> str: + def get_latest_layer_info(self, layer_name: str) -> Dict: """Returns the latest matching version of the layer with 'layer_name'.""" layer = self._find(layer_name) return layer['LatestMatchingVersion'] if layer else {} @@ -101,29 +103,28 @@ def get_latest_layer_info(self, layer_name: str) -> str: class LambdaLayers(): """"Class used to manage the lambda supervisor layer.""" - _SUPERVISOR_LAYER_NAME = 'faas-supervisor' - @lazy_property def layer(self): """Property used to manage the lambda layers.""" layer = Layer(self.lambda_client) return layer - def __init__(self, lambda_client, supervisor_version: str) -> None: + def __init__(self, function: Dict, lambda_client: LambdaClient) -> None: + self.function = function self.lambda_client = lambda_client - self.supervisor_version = supervisor_version + self.layer_name = self.function.get('supervisor').get('layer_name') def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: - return {'LayerName' : self._SUPERVISOR_LAYER_NAME, - 'Description' : self.supervisor_version, + return {'LayerName' : self.layer_name, + 'Description' : self.function.get('supervisor').get('version'), 'Content' : {'ZipFile': FileUtils.read_file(layer_zip_path, mode="rb")}, - 'LicenseInfo' : 'Apache 2.0'} + 'LicenseInfo' : self.function.get('supervisor').get('license_info')} def _create_layer(self) -> None: tmp_zip_path, layer_code_path = _create_tmp_folders() - layer_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), - f"{self._SUPERVISOR_LAYER_NAME}.zip") - parent_folder = _download_supervisor(self.supervisor_version, tmp_zip_path) + layer_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), f"{self.layer_name}.zip") + parent_folder = _download_supervisor(self.function.get('supervisor').get('version'), + tmp_zip_path) _copy_supervisor_files(parent_folder, tmp_zip_path, layer_code_path) _copy_extra_files(parent_folder, tmp_zip_path, layer_code_path) _create_layer_zip(layer_zip_path, layer_code_path) @@ -131,37 +132,25 @@ def _create_layer(self) -> None: FileUtils.delete_file(layer_zip_path) def _create_supervisor_layer(self) -> None: - logger.info("Creating faas-supervisor layer.") + logger.info(f"Creating '{self.layer_name}' layer.") self._create_layer() - logger.info("Faas-supervisor layer created.") + logger.info(f"'{self.layer_name}' layer created.") def _update_supervisor_layer(self) -> None: - logger.info("Updating faas-supervisor layer.") + logger.info(f"Updating '{self.layer_name}' layer.") self._create_layer() - logger.info("Faas-supervisor layer updated.") - - def print_layers_info(self) -> None: - """Prints the lambda layers information.""" - layers_info = self.lambda_client.list_layers() - headers = ['NAME', 'VERSION', 'ARN', 'RUNTIMES'] - table = [] - for layer in layers_info: - table.append([layer.get('LayerName', ""), - layer.get('LatestMatchingVersion', {}).get('Version', -1), - layer.get('LayerArn', ""), - layer.get('LatestMatchingVersion', {}).get('CompatibleRuntimes', '-')]) - print(tabulate(table, headers)) + logger.info(f"'{self.layer_name}' layer updated.") def get_latest_supervisor_layer_arn(self) -> str: """Returns the ARN of the latest supervisor layer.""" - layer_info = self.layer.get_latest_layer_info(self._SUPERVISOR_LAYER_NAME) + layer_info = self.layer.get_latest_layer_info(self.layer_name) return layer_info.get('LayerVersionArn', "") def check_faas_supervisor_layer(self): """Checks if the supervisor layer exists, if not, creates the layer. If the layer exists and it's not updated, updates the layer.""" # Get the layer information - layer_info = self.layer.get_latest_layer_info(self._SUPERVISOR_LAYER_NAME) + layer_info = self.layer.get_latest_layer_info(self.layer_name) # Compare supervisor versions if layer_info and 'Description' in layer_info: # If the supervisor layer version is lower than the passed version, @@ -170,7 +159,7 @@ def check_faas_supervisor_layer(self): self.supervisor_version) < 0: self._update_supervisor_layer() else: - logger.info("Using existent 'faas-supervisor' layer") + logger.info(f"Using existent '{self.layer_name}' layer") else: # Layer not found, we have to create it self._create_supervisor_layer() diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index eac9eb11..58645990 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -69,15 +69,19 @@ def _print_generic_response(response, output_type, aws_output, text_message=None logger.info_json(output) -def parse_lambda_function_creation_response(response, function_name, access_key, output_type): +def parse_lambda_function_creation_response(response, function, access_key): if response: + function_name = function.get('lambda', {}).get('name', '') + output_type = function.get('lambda', {}).get('output', '') aws_output = 'LambdaOutput' text_message = f"Function '{function_name}' successfully created." - json_message = {aws_output : {'AccessKey' : access_key, - 'FunctionArn' : response['FunctionArn'], - 'Timeout' : response['Timeout'], - 'MemorySize' : response['MemorySize'], - 'FunctionName' : response['FunctionName']}} + json_message = {aws_output : { + 'AccessKey' : access_key, + 'FunctionArn' : response['FunctionArn'], + 'Timeout' : response['Timeout'], + 'MemorySize' : response['MemorySize'], + 'FunctionName' : response['FunctionName']} + } _print_generic_response(response, output_type, aws_output, text_message, json_output=json_message) diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index e6375263..1eb6b9d8 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -23,7 +23,8 @@ class S3(GenericClient): def __init__(self, aws_properties): - super().__init__(aws_properties) + super().__init__(aws_properties.get('lambda')) + if hasattr(self.aws, 's3'): if type(self.aws.s3) is dict: self.aws.s3 = S3Properties(self.aws.s3) diff --git a/scar/providers/aws/validators.py b/scar/providers/aws/validators.py index 89f4c7c9..6ff44db3 100644 --- a/scar/providers/aws/validators.py +++ b/scar/providers/aws/validators.py @@ -31,13 +31,14 @@ class AWSValidator(GenericValidator): @classmethod def validate_kwargs(cls, **kwargs): - prov_args = kwargs['aws'] - if 'iam' in prov_args: - cls.validate_iam(prov_args['iam']) - if 'lambda' in prov_args: - cls.validate_lambda(prov_args['lambda']) - if 'batch' in prov_args: - cls.validate_batch(prov_args['batch']) + aws_functions = kwargs.get('functions', {}).get('aws', {}) + for function in aws_functions: + if 'iam' in function: + cls.validate_iam(function['iam']) + if 'lambda' in function: + cls.validate_lambda(function['lambda']) + if 'batch' in function: + cls.validate_batch(function['batch']) @staticmethod def validate_iam(iam_properties): diff --git a/scar/scarcli.py b/scar/scarcli.py index c250c786..a866ef55 100755 --- a/scar/scarcli.py +++ b/scar/scarcli.py @@ -20,11 +20,11 @@ from scar.cmdtemplate import Commands from scar.parser.cfgfile import ConfigFileParser from scar.parser.cli import CommandParser -from scar.parser.yaml import YamlParser from scar.providers.aws.controller import AWS +from scar.utils import FileUtils +import scar.parser.fdl as fdl import scar.exceptions as excp import scar.logger as logger -from scar.utils import DataTypesUtils class ScarCLI(Commands): @@ -69,14 +69,22 @@ def parse_arguments(self): That is, the CMD parameter will override any other configuration, and the YAML parameters will override the SCAR.CONF settings """ - merged_args = ConfigFileParser().get_properties() + config_args = ConfigFileParser().get_properties() cmd_args = CommandParser(self).parse_arguments() if 'conf_file' in cmd_args['scar'] and cmd_args['scar']['conf_file']: - yaml_args = YamlParser(cmd_args['scar']).parse_arguments() - merged_args = DataTypesUtils.merge_dicts(yaml_args, merged_args) - merged_args = DataTypesUtils.merge_dicts(cmd_args, merged_args) - self.cloud_provider.parse_arguments(**merged_args) - merged_args['scar']['func']() + yaml_args = FileUtils.load_yaml(cmd_args['scar']['conf_file']) + # YAML >> SCAR.CONF + merged_args = fdl.merge_conf(config_args, yaml_args) + merged_args = fdl.merge_cmd_yaml(cmd_args, merged_args) + else: + # CMD >> SCAR.CONF + merged_args = fdl.merge_conf(config_args, cmd_args) + import pprint + pprint.pprint(merged_args) + self.cloud_provider.parse_arguments(merged_args) + #merged_args.get('scar').get('func')() + pprint.pprint(merged_args) + def main(): logger.init_execution_trace() @@ -88,5 +96,6 @@ def main(): logger.exception(excp) logger.end_execution_trace_with_errors() + if __name__ == "__main__": main() diff --git a/scar/utils.py b/scar/utils.py index a6a82ed8..80ce09a1 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -23,16 +23,18 @@ import tempfile import uuid import sys +import yaml from typing import Optional, Dict, List, Generator, Union, Any from distutils import dir_util from packaging import version import scar.logger as logger import scar.http.request as request -from scar.exceptions import GitHubTagNotFoundError +from scar.exceptions import GitHubTagNotFoundError, YamlFileNotFoundError GITHUB_USER = 'grycap' GITHUB_SUPERVISOR_PROJECT = 'faas-supervisor' + def lazy_property(func): # Skipped type hinting: https://github.com/python/mypy/issues/3157 """ A decorator that makes a property lazy-evaluated.""" @@ -74,8 +76,8 @@ def delete_environment_variable(variable: str) -> None: del os.environ[variable] @staticmethod - def execute_command_with_msg(command: List[str], cmd_wd: Optional[str] = None, - cli_msg: str = '') -> str: + def execute_command_with_msg(command: List[str], cmd_wd: Optional[str]=None, + cli_msg: str='') -> str: """Execute the specified command and return the result.""" cmd_out = subprocess.check_output(command, cwd=cmd_wd).decode('utf-8') logger.debug(cmd_out) @@ -115,14 +117,29 @@ def merge_dicts(dict1: Dict, dict2: Dict) -> Dict: 'dict2' has precedence over 'dict1'.""" for key, val in dict2.items(): if val is not None: - if key not in dict1: - dict1[key] = val - elif isinstance(val, dict): + if isinstance(val, dict) and key in dict1: dict1[key] = DataTypesUtils.merge_dicts(dict1[key], val) - elif isinstance(val, list): + elif isinstance(val, list) and key in dict1: dict1[key] += val + else: + dict1[key] = val return dict1 + @staticmethod + def merge_dicts_with_copy(dict1: Dict, dict2: Dict) -> Dict: + """Merge 'dict1' and 'dict2' dicts into a new Dict. + 'dict2' has precedence over 'dict1'.""" + result = dict1.copy() + for key, val in dict2.items(): + if val is not None: + if isinstance(val, dict) and key in result: + result[key] = DataTypesUtils.merge_dicts_with_copy(result[key], val) + elif isinstance(val, list) and key in result: + result[key] += val + else: + result[key] = val + return result + @staticmethod def divide_list_in_chunks(elements: List, chunk_size: int) -> Generator[List, None, None]: """Yield successive n-sized chunks from th elements list.""" @@ -216,7 +233,7 @@ def get_file_size(file_path: str) -> int: @staticmethod def create_file_with_content(path: str, content: Optional[Union[str, bytes]], - mode: str = 'w') -> None: + mode: str='w') -> None: """Creates a new file with the passed content. If the content is a dictionary, first is converted to a string.""" with open(path, mode) as fwc: @@ -225,7 +242,7 @@ def create_file_with_content(path: str, fwc.write(content) @staticmethod - def read_file(file_path: str, mode: str = 'r') -> Optional[Union[str, bytes]]: + def read_file(file_path: str, mode: str='r') -> Optional[Union[str, bytes]]: """Reads the whole specified file and returns the content.""" with open(file_path, mode) as content_file: return content_file.read() @@ -256,7 +273,7 @@ def extract_tar_gz(tar_path: str, destination_path: str) -> None: tar.extractall(path=destination_path) @staticmethod - def unzip_folder(zip_path: str, folder_where_unzip_path: str, msg: str = '') -> None: + def unzip_folder(zip_path: str, folder_where_unzip_path: str, msg: str='') -> None: """Must use the unzip binary to preserve the file properties and the symlinks.""" zip_exe = '/usr/bin/unzip' SysUtils.execute_command_with_msg([zip_exe, zip_path], @@ -264,7 +281,7 @@ def unzip_folder(zip_path: str, folder_where_unzip_path: str, msg: str = '') -> cli_msg=msg) @staticmethod - def zip_folder(zip_path: str, folder_to_zip_path: str, msg: str = '') -> None: + def zip_folder(zip_path: str, folder_to_zip_path: str, msg: str='') -> None: """Must use the zip binary to preserve the file properties and the symlinks.""" zip_exe = '/usr/bin/zip' SysUtils.execute_command_with_msg([zip_exe, '-r9y', zip_path, '.'], @@ -272,10 +289,19 @@ def zip_folder(zip_path: str, folder_to_zip_path: str, msg: str = '') -> None: cli_msg=msg) @staticmethod - def is_file(file_path): + def is_file(file_path: str): """Test whether a path is a regular file.""" return os.path.isfile(file_path) + @staticmethod + def load_yaml(file_path: str) -> Dict: + """Returns the content of a YAML file as a Dict.""" + if os.path.isfile(file_path): + with open(file_path) as cfg_file: + return yaml.safe_load(cfg_file) + else: + raise YamlFileNotFoundError(file_path=file_path) + class StrUtils: """Common methods for string management.""" @@ -303,12 +329,12 @@ def utf8_to_base64_string(value: str) -> str: """Encode a 'utf-8' string using Base64 and return the encoded value as a string.""" return StrUtils.encode_base64(bytes(value, 'utf-8')).decode('utf-8') - + @staticmethod def bytes_to_base64str(value, encoding='utf-8') -> str: """Encode a 'utf-8' string using Base64 and return the encoded value as a string.""" - return StrUtils.encode_base64(value).decode(encoding) + return StrUtils.encode_base64(value).decode(encoding) @staticmethod def dict_to_base64_string(value: Dict) -> str: @@ -365,7 +391,7 @@ def exists_release_in_repo(user: str, project: str, tag_name: str) -> bool: @staticmethod def get_asset_url(user: str, project: str, asset_name: str, - tag_name: str = 'latest') -> Optional[str]: + tag_name: str='latest') -> Optional[str]: """Get the download asset url from the specified github tagged project.""" if tag_name == 'latest': url = f'https://api.github.com/repos/{user}/{project}/releases/latest' @@ -382,7 +408,7 @@ def get_asset_url(user: str, project: str, asset_name: str, return None @staticmethod - def get_source_code_url(user: str, project: str, tag_name: str = 'latest') -> str: + def get_source_code_url(user: str, project: str, tag_name: str='latest') -> str: """Get the source code's url from the specified github tagged project.""" source_url = "" repo_url = "" diff --git a/scar/version.py b/scar/version.py index a0cd79a3..b34bc1f3 100644 --- a/scar/version.py +++ b/scar/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '3.2.2' \ No newline at end of file +__version__ = '3.2.4' \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/__init__.py b/test/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/parser/__init__.py b/test/functional/parser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/parser/fdl.py b/test/functional/parser/fdl.py new file mode 100644 index 00000000..587a9ab2 --- /dev/null +++ b/test/functional/parser/fdl.py @@ -0,0 +1,52 @@ +# Copyright (C) GRyCAP - I3M - UPV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from scar.parser.fdl import FDLParser + + +class Test(unittest.TestCase): + + def testFDL(self): + ''' Expected return value (except random ids): + [{'env_vars':{ + 'STORAGE_AUTH_MINIO_PASS_TMPXCMCNA9S': 'mpass', + 'STORAGE_AUTH_MINIO_USER_TMPXCMCNA9S': 'muser', + 'STORAGE_PATH_INPUT_TMPOTOWSDYE': 's3-bucket/test1', + 'STORAGE_PATH_INPUT_TMPXCMCNA9S': 'my-bucket/test', + 'STORAGE_PATH_OUTPUT_TMPOTOWSDYE': 's3-bucket/test1-output', + 'STORAGE_PATH_OUTPUT_TMPXCMCNA9S': 'my-bucket/test-output', + 'STORAGE_PATH_SUFIX_TMPOTOWSDYE': 'avi', + 'STORAGE_PATH_SUFIX_TMPXCMCNA9S': 'wav:srt'}, + 'name': 'function1'}, + {'env_vars': { + 'STORAGE_AUTH_MINIO_PASS_TMPXCMCNA9S': 'mpass', + 'STORAGE_AUTH_MINIO_USER_TMPXCMCNA9S': 'muser', + 'STORAGE_PATH_INPUT_TMPXCMCNA9S': 'my-bucket2/test', + 'STORAGE_PATH_OUTPUT_TMPXCMCNA9S': 'my-bucket2/test-output', + 'STORAGE_PATH_PREFIX_TMPXCMCNA9S': 'my_file'}, + 'name': 'function2'}] + ''' + result = FDLParser().parse_yaml('fdl.yaml') + self.assertEqual(len(result), 2) + for function in result: + self.assertTrue(('name' in function) and ('env_vars' in function)) + if function['name'] == 'function1': + self.assertEqual(len(function['env_vars'].items()), 8) + elif function['name'] == 'function2': + self.assertEqual(len(function['env_vars'].items()), 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/functional/parser/fdl.yaml b/test/functional/parser/fdl.yaml new file mode 100644 index 00000000..6d432a70 --- /dev/null +++ b/test/functional/parser/fdl.yaml @@ -0,0 +1,39 @@ +functions: + - name: function1 + input: + - name: minio-local # Match with automatically generated id -> 123 + path: my-bucket/test #STORAGE_PATH_INTPUT_123=my-bucket/test + - name: s3-bucket # Match with automatically generated id -> 456 + path: s3-bucket/test1 #STORAGE_PATH_INTPUT_456=s3-bucket/test1 + output: + - name: minio-local # Match with automatically generated id -> 123 + path: my-bucket/test-output #STORAGE_PATH_OUTPUT_123=my-bucket/test-output + files: + sufix: #STORAGE_PATH_SUFIX_123=wav:srt + - wav + - srt + - name: s3-bucket # Match with automatically generated id -> 456 + path: s3-bucket/test1-output #STORAGE_PATH_OUTPUT_456=s3-bucket/test1-output + files: + sufix: # Possible values: 'prefix', 'sufix' + - avi #STORAGE_PATH_SUFIX_123=avi + - name: function2 + input: + - name: minio-local # Match with automatically generated id -> 123 + path: my-bucket2/test #STORAGE_PATH_INTPUT_123=my-bucket/test + output: + - name: minio-local # Match with automatically generated id -> 123 + path: my-bucket2/test-output #STORAGE_PATH_OUTPUT_123=my-bucket/test-output + files: + prefix: #STORAGE_PATH_SUFIX_123=wav:srt + - my_file + + +storages: + - name: minio-local # Generate random id -> 123 + type: minio # Possible values: 'minio', 's3', 'onedata' + auth: # Possible values: 'user', 'pass', 'token', 'space', 'host' + user: muser #STORAGE_AUTH_MINIO_USER_123=muser + pass: mpass #STORAGE_AUTH_MINIO_PASS_123=mpass + - name: s3-bucket # Generate random id -> 456 + type: S3 diff --git a/video-process.yaml b/video-process.yaml new file mode 100644 index 00000000..f7ffc07d --- /dev/null +++ b/video-process.yaml @@ -0,0 +1,35 @@ +functions: + aws: + - lambda: + name: scar-batch-ffmpeg-split + image: grycap/ffmpeg + init_script: split-video.sh + execution_mode: batch + log_level: debug + input: + - storage_name: s3-bucket + path: scar-ffmpeg + output: + - storage_name: s3-bucket + path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output + - lambda: + name: scar-lambda-darknet + image: grycap/darknet + memory: 3008 + log_level: debug + init_script: yolo-sample-object-detection.sh + input: + - storage_name: s3-bucket + path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output + output: + - storage_name: s3-bucket + path: scar-ffmpeg/scar-batch-ffmpeg-split/image-output + +storages: + s3: + - name: s3-bucket + minio: + - name: minio-bucket + auth: + user: muser + pass: mpass From a1c39766cadc65f0473162ace1c630dcf73356df Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Fri, 8 Nov 2019 13:47:19 +0100 Subject: [PATCH 02/41] Update definition --- scar/parser/fdl2.yaml | 54 ++++++++++++++++++++++++------------------- video-process.yaml | 16 +++++-------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/scar/parser/fdl2.yaml b/scar/parser/fdl2.yaml index c9b9c60d..058de053 100644 --- a/scar/parser/fdl2.yaml +++ b/scar/parser/fdl2.yaml @@ -1,40 +1,46 @@ functions: -- aws: - - name: function1 - input: - - name: minio-local - path: my-bucket/test - - name: s3-bucket - path: s3-bucket/test1 - output: - - name: minio-local + aws: + - lambda: + name: function1 + input: + - storage: minio + path: my-bucket/test + - storage: s3 + path: s3-bucket/test1 + output: + - storage: minio path: my-bucket/test-output files: sufix: - wav - srt - - name: s3-bucket + - storage: s3 path: s3-bucket/test1-output files: suffix: - avi - - name: function2 - execution_mode: batch - input: - - name: minio-local + - lambda: + name: function2 + execution_mode: batch + input: + - storage: minio path: my-bucket2/test - output: - - name: minio-local + output: + - storage: minio path: my-bucket2/test-output files: - prefix: # Possible values: 'prefix', 'suffix' + prefix: # Possible values: 'prefix', 'suffix' - my_file storages: -- minio: # Possible values: 'minio', 's3', 'onedata' - - name: minio-local - auth: - user: muser # Possible values: 'user', 'pass', 'token', 'space', 'host' - pass: mpass -- s3: - - name: s3-bucket + minio: # Possible values: 'minio', 's3', 'onedata' + access_key: muser # Possible values: 'access_key', 'secret_key', 'region' + secret_key: mpass + s3: + access_key: awsuser # Possible values: 'access_key', 'secret_key', 'region' (if not defined supervisor will + secret_key: awskey # try to create the boto3 client using the function permissions (in AWS Lambda environment)) + region: us-east-1 + onedata: + oneprovider_host: op-host # Possible values: 'oneprovider_host', 'token', 'space' + token: mytoken + space: onedata_space diff --git a/video-process.yaml b/video-process.yaml index f7ffc07d..199dd625 100644 --- a/video-process.yaml +++ b/video-process.yaml @@ -7,10 +7,10 @@ functions: execution_mode: batch log_level: debug input: - - storage_name: s3-bucket + - storage: s3 path: scar-ffmpeg output: - - storage_name: s3-bucket + - storage: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output - lambda: name: scar-lambda-darknet @@ -19,17 +19,13 @@ functions: log_level: debug init_script: yolo-sample-object-detection.sh input: - - storage_name: s3-bucket + - storage: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output output: - - storage_name: s3-bucket + - storage: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/image-output storages: - s3: - - name: s3-bucket minio: - - name: minio-bucket - auth: - user: muser - pass: mpass + access_key: muser + secret_key: mpass From ddffa99c5ef8cab43d3cffb2f386b4405718ca3e Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 20 Nov 2019 10:12:27 +0100 Subject: [PATCH 03/41] WIP - Update init method --- scar/cmdtemplate.py | 4 - scar/parser/cfgfile.py | 14 +-- scar/parser/cli/__init__.py | 43 +++------ scar/parser/cli/subparsers.py | 48 ++++++---- scar/providers/aws/apigateway.py | 11 ++- scar/providers/aws/batchfunction.py | 86 ++++++++--------- scar/providers/aws/cloudwatchlogs.py | 20 ++-- scar/providers/aws/controller.py | 125 ++++++++++++++++--------- scar/providers/aws/functioncode.py | 132 ++++++++++++++------------- scar/providers/aws/lambdafunction.py | 116 +++++++++++------------ scar/providers/aws/lambdalayers.py | 3 + scar/providers/aws/resourcegroups.py | 3 + scar/providers/aws/response.py | 12 +-- scar/providers/aws/s3.py | 48 +++++----- scar/providers/aws/udocker.py | 74 +++++++-------- scar/scarcli.py | 87 ++++++------------ scar/utils.py | 22 ++++- video-process.yaml | 10 +- 18 files changed, 442 insertions(+), 416 deletions(-) diff --git a/scar/cmdtemplate.py b/scar/cmdtemplate.py index 61f43695..a2d80a24 100644 --- a/scar/cmdtemplate.py +++ b/scar/cmdtemplate.py @@ -65,7 +65,3 @@ def put(self): @abc.abstractmethod def get(self): pass - - @abc.abstractmethod - def parse_arguments(self, args): - pass diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index 0d250f10..4e8ad4b3 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -39,19 +39,19 @@ "asynchronous": False, "log_type": "Tail", "log_level": "INFO", - "environment": {"Variables": {}}, + "environment": { + "Variables": { + "UDOCKER_BIN" : "/opt/udocker/bin/", + "UDOCKER_LIB" : "/opt/udocker/lib/", + "UDOCKER_DIR" : "/tmp/shared/udocker", + "UDOCKER_EXEC": "/opt/udocker/udocker.py"}}, "deployment": { "max_payload_size": 52428800, "max_s3_payload_size": 262144000 }, "container": { "environment_variables": {}, - "timeout_threshold": 10, - "udocker": { - "bin": "/opt/udocker/bin/", - "dir": "/tmp/shared/udocker", - "exec": "/opt/udocker/udocker.py", - "lib": "/opt/udocker/lib/"} + "timeout_threshold": 10 }, # Must be a Github tag or "latest" "supervisor": { diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index f049462f..9aea1ed2 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -19,7 +19,8 @@ from typing import Dict, List from scar.parser.cli.subparsers import Subparsers from scar.parser.cli.parents import * -from scar.utils import DataTypesUtils, StrUtils +from scar.utils import DataTypesUtils, StrUtils, FileUtils +from scar.cmdtemplate import CallType import scar.exceptions as excp import scar.logger as logger import scar.version as version @@ -47,10 +48,10 @@ def _set_args(args: Dict, key: str, val: str) -> None: def _parse_scar_args(cmd_args: Dict) -> Dict: - scar_args = ['func', 'conf_file', 'json', + scar_args = ['conf_file', 'json', 'verbose', 'path', 'preheat', 'execution_mode', - 'output_file', 'supervisor_version'] + 'output_file', 'supervisor_version', 'all'] return {'scar' : DataTypesUtils.parse_arg_list(scar_args, cmd_args)} @@ -63,7 +64,7 @@ def _parse_lambda_args(cmd_args: Dict) -> Dict: lambda_arg_list = ['name', 'asynchronous', 'init_script', 'run_script', 'c_args', 'memory', 'timeout', 'timeout_threshold', 'image', 'image_file', 'description', 'lambda_role', 'extra_payload', ('environment', 'environment_variables'), - 'layers', 'lambda_environment', 'list_layers', 'all', 'log_level'] + 'layers', 'lambda_environment', 'list_layers', 'log_level'] lambda_args = DataTypesUtils.parse_arg_list(lambda_arg_list, cmd_args) # Standardize log level if defined if "log_level" in lambda_args: @@ -74,10 +75,10 @@ def _parse_lambda_args(cmd_args: Dict) -> Dict: def _get_lambda_environment_variables(lambda_args: Dict) -> None: - lambda_env_vars = {} + lambda_env_vars = {"environment": {"Variables": {}}, + "container": {'environment_variables' : {}}} if "environment_variables" in lambda_args: # These variables define the udocker container environment variables - lambda_env_vars['container'] = {'environment_variables' : {}} for env_var in lambda_args["environment_variables"]: key_val = env_var.split("=") # Add an specific prefix to be able to find the container variables defined by the user @@ -86,14 +87,13 @@ def _get_lambda_environment_variables(lambda_args: Dict) -> None: if "extra_payload" in lambda_args: lambda_env_vars['container']['extra_payload'] = f"/var/task" if "init_script" in lambda_args: - lambda_env_vars['container']['init_script'] = f"/var/task/init_script.sh" + lambda_env_vars['container']['init_script'] = f"/var/task/{FileUtils.get_file_name(lambda_args['init_script'])}" if "image" in lambda_args: lambda_env_vars['container']['image'] = lambda_args.get('image') del(lambda_args['image']) if "lambda_environment" in lambda_args: # These variables define the lambda environment variables - lambda_env_vars['environment'] = {'Variables' : {}} for env_var in lambda_args["lambda_environment"]: key_val = env_var.split("=") lambda_env_vars['environment']['Variables'][f'{key_val[0]}'] = key_val[1] @@ -122,7 +122,7 @@ def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: if s3_args: s3_id = StrUtils.get_random_uuid4_str() if 'deployment_bucket' in s3_args: - aws_args['lambda']['deployment_bucket'] = s3_args['deployment_bucket'] + aws_args['lambda']['deployment'] = {'bucket': s3_args['deployment_bucket']} if 'input_bucket' in s3_args: aws_args['lambda']['input'] = [{'storage_name' : s3_id, 'path': s3_args['input_bucket']}] if 'output_bucket' in s3_args: @@ -163,23 +163,16 @@ def _create_parent_parsers() -> Dict: class CommandParser(): """Class to manage the SCAR CLI commands.""" - def __init__(self, scar_cli): - self.scar_cli = scar_cli + def __init__(self): self.parser = _create_main_parser() self.parent_parsers = _create_parent_parsers() self._add_subparsers() def _add_subparsers(self) -> None: subparsers = Subparsers(self.parser.add_subparsers(title='Commands'), self.parent_parsers) - subparsers.add_subparser('init', self.scar_cli.init) - subparsers.add_subparser('invoke', self.scar_cli.invoke) - subparsers.add_subparser('run', self.scar_cli.run) - subparsers.add_subparser('update', self.scar_cli.update) - subparsers.add_subparser('rm', self.scar_cli.rm) - subparsers.add_subparser('ls', self.scar_cli.ls) - subparsers.add_subparser('log', self.scar_cli.log) - subparsers.add_subparser('put', self.scar_cli.put) - subparsers.add_subparser('get', self.scar_cli.get) + # We need to define a subparser for each command defined in the CallType class + for cmd in CallType: + subparsers.add_subparser(cmd.value) @excp.exception(logger) def parse_arguments(self) -> Dict: @@ -189,20 +182,12 @@ def parse_arguments(self) -> Dict: if cmd_args.version: print(f"SCAR {version.__version__}") sys.exit(0) - cmd_args = vars(cmd_args) if 'func' not in cmd_args: raise excp.MissingCommandError() scar_args = _parse_scar_args(cmd_args) aws_args = _parse_aws_args(cmd_args) - import pprint - print("--------------------------------------------SCAR---------------------------------------") - pprint.pprint(scar_args) - print("--------------------------------------------AWS---------------------------------------") - pprint.pprint(aws_args) - print("-----------------------------------------------------------------------------------------") - - return DataTypesUtils.merge_dicts_with_copy(scar_args, aws_args) + return cmd_args['func'], DataTypesUtils.merge_dicts_with_copy(scar_args, aws_args) except AttributeError as aerr: logger.error("Incorrect arguments: use scar -h to see the options available", f"Error parsing arguments: {aerr}") diff --git a/scar/parser/cli/subparsers.py b/scar/parser/cli/subparsers.py index 3d834e5d..90a2146d 100644 --- a/scar/parser/cli/subparsers.py +++ b/scar/parser/cli/subparsers.py @@ -36,15 +36,15 @@ def __init__(self, subparser, parents): def _get_parents(self, parent_sublist): return [self.parent_parsers.get(parent, "") for parent in parent_sublist] - def add_subparser(self, name, scar_func): - getattr(self, f'_add_{name}_parser')(scar_func) + def add_subparser(self, name): + getattr(self, f'_add_{name}_parser')() - def _add_init_parser(self, scar_func): + def _add_init_parser(self): init = self.subparser.add_parser('init', parents=self._get_parents(INIT_UPDATE_PARENTS), help="Create lambda function") # Set default function - init.set_defaults(func=scar_func) + init.set_defaults(func="init") # Lambda conf group = init.add_mutually_exclusive_group(required=True) group.add_argument("-i", "--image", @@ -69,12 +69,12 @@ def _add_init_parser(self, scar_func): init.add_argument("-api", "--api-gateway-name", help="API Gateway name created to launch the lambda function") - def _add_invoke_parser(self, scar_func): + def _add_invoke_parser(self): invoke = self.subparser.add_parser('invoke', parents=self._get_parents(INVOKE_PARENTS), help="Call a lambda function using an HTTP request") # Set default function - invoke.set_defaults(func=scar_func) + invoke.set_defaults(func='invoke') group = invoke.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -89,21 +89,23 @@ def _add_invoke_parser(self, scar_func): "you can pass the parameters here (i.e. '{\"key1\": " "\"value1\", \"key2\": [\"value2\", \"value3\"]}').")) - def _add_update_parser(self, scar_func): + def _add_update_parser(self): update = self.subparser.add_parser('update', parents=self._get_parents(INIT_UPDATE_PARENTS), help="Update function properties") - update.set_defaults(func=scar_func) + # Set default function + update.set_defaults(func='update') group = update.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") group.add_argument("-a", "--all", help="Update all lambda functions", action="store_true") group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - def _add_run_parser(self, scar_func): + def _add_run_parser(self): run = self.subparser.add_parser('run', parents=self._get_parents(RUN_PARENTS), help="Deploy function") - run.set_defaults(func=scar_func) + # Set default function + run.set_defaults(func='run') group = run.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") @@ -112,11 +114,12 @@ def _add_run_parser(self, scar_func): nargs=argparse.REMAINDER, help="Arguments passed to the container.") - def _add_rm_parser(self, scar_func): + def _add_rm_parser(self): rm = self.subparser.add_parser('rm', parents=self._get_parents(RM_LS_PARENTS), help="Delete function") - rm.set_defaults(func=scar_func) + # Set default function + rm.set_defaults(func='rm') group = rm.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -126,11 +129,12 @@ def _add_rm_parser(self, scar_func): group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - def _add_log_parser(self, scar_func): + def _add_log_parser(self): log = self.subparser.add_parser('log', parents=self._get_parents(LOG_PARENTS), help="Show the logs for the lambda function") - log.set_defaults(func=scar_func) + # Set default function + log.set_defaults(func='log') group = log.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -142,11 +146,12 @@ def _add_log_parser(self, scar_func): log.add_argument("-ri", "--request-id", help="Return the output for the request id specified.") - def _add_ls_parser(self, scar_func): + def _add_ls_parser(self): ls = self.subparser.add_parser('ls', parents=self._get_parents(RM_LS_PARENTS), help="List lambda functions") - ls.set_defaults(func=scar_func) + # Set default function + ls.set_defaults(func='ls') # S3 args ls.add_argument("-b", "--bucket", help="Show bucket files") # Layer args @@ -154,14 +159,17 @@ def _add_ls_parser(self, scar_func): help="Show lambda layers information", action="store_true") - def _add_put_parser(self, scar_func): + def _add_put_parser(self): put = self.subparser.add_parser('put', parents=self._get_parents(PUT_GET_PARENTS), help="Upload file(s) to bucket") - put.set_defaults(func=scar_func) + # Set default function + put.set_defaults(func='put') - def _add_get_parser(self, scar_func): + def _add_get_parser(self): get = self.subparser.add_parser('get', parents=self._get_parents(PUT_GET_PARENTS), help="Download file(s) from bucket") - get.set_defaults(func=scar_func) + # Set default function + get.set_defaults(func='get') + diff --git a/scar/providers/aws/apigateway.py b/scar/providers/aws/apigateway.py index 2a876216..e6dd2360 100644 --- a/scar/providers/aws/apigateway.py +++ b/scar/providers/aws/apigateway.py @@ -22,7 +22,7 @@ class APIGateway(GenericClient): def __init__(self, aws_properties: Dict): super().__init__(aws_properties.get('api_gateway', {})) - self.aws = aws_properties + self._aws = aws_properties self.api = self.aws_properties.get('api_gateway', {}) def _get_common_args(self) -> Dict: @@ -36,9 +36,9 @@ def _get_method_args(self) -> Dict: def _get_integration_args(self) -> Dict: integration_args = self.api.get('integration', {}) uri_args = {'api_region': self.api.get('region', ''), - 'lambda_region': self.aws.get('lambda', {}).get('region', ''), - 'account_id': self.aws.get('iam', {}).get('account_id', ''), - 'function_name': self.aws.get('lambda', {}).get('name', '')} + 'lambda_region': self._aws.get('lambda', {}).get('region', ''), + 'account_id': self._aws.get('iam', {}).get('account_id', ''), + 'function_name': self._aws.get('lambda', {}).get('name', '')} integration_args['uri'] = integration_args['uri'].format(**uri_args) return self._get_common_args().update(integration_args) @@ -53,6 +53,9 @@ def _get_resource_id(self) -> str: def _set_api_gateway_id(self, api_info: Dict) -> None: self.api['id'] = api_info.get('id', '') + # We store the parameter in the lambda configuration that + # is going to be uploaded to the Lambda service + self._aws['lambda']['api_gateway_id'] = api_info.get('id', '') def _set_resource_info_id(self, resource_info: Dict) -> None: self.api['resource_id'] = resource_info.get('id', '') diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index 960c0153..47c2b7c6 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -31,28 +31,28 @@ class Batch(GenericClient): @lazy_property def launch_templates(self): - launch_templates = LaunchTemplates(self.aws, self.supervisor_version) + launch_templates = LaunchTemplates(self._aws, self.supervisor_version) return launch_templates def __init__(self, aws_properties, supervisor_version): super().__init__(aws_properties) self.supervisor_version = supervisor_version - self.aws.batch.instance_role = (f"arn:aws:iam::{self.aws.account_id}:" + self._aws.batch.instance_role = (f"arn:_aws:iam::{self._aws.account_id}:" "instance-profile/ecsInstanceRole") - self.aws.batch.service_role = (f"arn:aws:iam::{self.aws.account_id}:" + self._aws.batch.service_role = (f"arn:_aws:iam::{self._aws.account_id}:" "role/service-role/AWSBatchServiceRole") - self.aws.batch.env_vars = [] + self._aws.batch.env_vars = [] def _set_required_environment_variables(self): - self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self.aws.lambdaf.name) + self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self._aws.lambdaf.name) self._set_batch_environment_variable('SCRIPT', self._get_user_script()) - if (hasattr(self.aws.lambdaf, 'environment_variables') and - self.aws.lambdaf.environment_variables): - self._add_custom_environment_variables(self.aws.lambdaf.environment_variables) - if (hasattr(self.aws.lambdaf, 'lambda_environment') and - self.aws.lambdaf.lambda_environment): - self._add_custom_environment_variables(self.aws.lambdaf.lambda_environment) - if hasattr(self.aws, "s3"): + if (hasattr(self._aws.lambdaf, 'environment_variables') and + self._aws.lambdaf.environment_variables): + self._add_custom_environment_variables(self._aws.lambdaf.environment_variables) + if (hasattr(self._aws.lambdaf, 'lambda_environment') and + self._aws.lambdaf.lambda_environment): + self._add_custom_environment_variables(self._aws.lambdaf.lambda_environment) + if hasattr(self._aws, "s3"): self._add_s3_environment_vars() def _add_custom_environment_variables(self, env_vars): @@ -65,25 +65,25 @@ def _add_custom_environment_variables(self, env_vars): def _set_batch_environment_variable(self, key, value): if key and value is not None: - self.aws.batch.env_vars.append({'name': key, 'value': value}) + self._aws.batch.env_vars.append({'name': key, 'value': value}) def _add_s3_environment_vars(self): provider_id = random.randint(1, 1000001) - if hasattr(self.aws.s3, "input_bucket"): + if hasattr(self._aws.s3, "input_bucket"): self._set_batch_environment_variable(f'STORAGE_PATH_INPUT_{provider_id}', - self.aws.s3.storage_path_input) - if hasattr(self.aws.s3, "output_bucket"): + self._aws.s3.storage_path_input) + if hasattr(self._aws.s3, "output_bucket"): self._set_batch_environment_variable(f'STORAGE_PATH_OUTPUT_{provider_id}', - self.aws.s3.storage_path_output) + self._aws.s3.storage_path_output) else: self._set_batch_environment_variable(f'STORAGE_PATH_OUTPUT_{provider_id}', - self.aws.s3.storage_path_input) + self._aws.s3.storage_path_input) self._set_batch_environment_variable(f'STORAGE_AUTH_S3_USER_{provider_id}', 'scar') def _get_user_script(self): script = '' - if hasattr(self.aws.lambdaf, "init_script"): - file_content = FileUtils.read_file(self.aws.lambdaf.init_script) + if hasattr(self._aws.lambdaf, "init_script"): + file_content = FileUtils.read_file(self._aws.lambdaf.init_script) script = StrUtils.utf8_to_base64_string(file_content) return script @@ -150,19 +150,19 @@ def _delete_valid_compute_environment(self, state, name): def _get_compute_env_args(self): return { - 'computeEnvironmentName': self.aws.lambdaf.name, - 'serviceRole': self.aws.batch.service_role, - 'type': self.aws.batch.compute_resources['type'], - 'state': self.aws.batch.compute_resources['state'], + 'computeEnvironmentName': self._aws.lambdaf.name, + 'serviceRole': self._aws.batch.service_role, + 'type': self._aws.batch.compute_resources['type'], + 'state': self._aws.batch.compute_resources['state'], 'computeResources': { - 'type': self.aws.batch.compute_resources['comp_type'], - 'minvCpus': self.aws.batch.compute_resources['min_v_cpus'], - 'maxvCpus': self.aws.batch.compute_resources['max_v_cpus'], - 'desiredvCpus': self.aws.batch.compute_resources['desired_v_cpus'], - 'instanceTypes': self.aws.batch.compute_resources['instance_types'], - 'subnets': self.aws.batch.compute_resources['subnets'], - 'securityGroupIds': self.aws.batch.compute_resources['security_group_ids'], - 'instanceRole': self.aws.batch.instance_role, + 'type': self._aws.batch.compute_resources['comp_type'], + 'minvCpus': self._aws.batch.compute_resources['min_v_cpus'], + 'maxvCpus': self._aws.batch.compute_resources['max_v_cpus'], + 'desiredvCpus': self._aws.batch.compute_resources['desired_v_cpus'], + 'instanceTypes': self._aws.batch.compute_resources['instance_types'], + 'subnets': self._aws.batch.compute_resources['subnets'], + 'securityGroupIds': self._aws.batch.compute_resources['security_group_ids'], + 'instanceRole': self._aws.batch.instance_role, 'launchTemplate': { 'launchTemplateName': _LAUNCH_TEMPLATE_NAME, 'version': str(self.launch_templates.get_launch_template_version()) @@ -172,27 +172,27 @@ def _get_compute_env_args(self): def _get_creations_job_queue_args(self): return { - 'computeEnvironmentOrder': [{'computeEnvironment': self.aws.lambdaf.name, + 'computeEnvironmentOrder': [{'computeEnvironment': self._aws.lambdaf.name, 'order': 1}, ], - 'jobQueueName': self.aws.lambdaf.name, + 'jobQueueName': self._aws.lambdaf.name, 'priority': 1, - 'state': self.aws.batch.compute_resources['state'], + 'state': self._aws.batch.compute_resources['state'], } def _get_resource_name(self, name=None): - return name if name else self.aws.lambdaf.name + return name if name else self._aws.lambdaf.name def _get_describe_compute_env_args(self, name_c=None): return {'computeEnvironments': [self._get_resource_name(name_c)]} def _get_job_definition_args(self): job_def_args = { - 'jobDefinitionName': self.aws.lambdaf.name, + 'jobDefinitionName': self._aws.lambdaf.name, 'type': 'container', 'containerProperties': { - 'image': self.aws.lambdaf.image, - 'memory': int(self.aws.batch.memory), - 'vcpus': int(self.aws.batch.vcpus), + 'image': self._aws.lambdaf.image, + 'memory': int(self._aws.batch.memory), + 'vcpus': int(self._aws.batch.vcpus), 'command': [ '/bin/sh', '-c', @@ -206,7 +206,7 @@ def _get_job_definition_args(self): 'name': 'supervisor-bin' } ], - 'environment': self.aws.batch.env_vars, + 'environment': self._aws.batch.env_vars, 'mountPoints': [ { 'containerPath': '/opt/faas-supervisor/bin', @@ -215,7 +215,7 @@ def _get_job_definition_args(self): ] } } - if self.aws.batch.enable_gpu: + if self._aws.batch.enable_gpu: job_def_args['containerProperties']['resourceRequirements'] = [ { 'value': '1', @@ -242,7 +242,7 @@ def create_batch_environment(self): self.client.create_job_queue(**creation_args) logger.info('Job queue successfully created.') creation_args = self._get_job_definition_args() - logger.info(f"Registering '{self.aws.lambdaf.name}' job definition.") + logger.info(f"Registering '{self._aws.lambdaf.name}' job definition.") return self.client.register_job_definition(**creation_args) def delete_compute_environment(self, name): diff --git a/scar/providers/aws/cloudwatchlogs.py b/scar/providers/aws/cloudwatchlogs.py index a9205804..ec5c3ef1 100644 --- a/scar/providers/aws/cloudwatchlogs.py +++ b/scar/providers/aws/cloudwatchlogs.py @@ -33,17 +33,17 @@ def get_log_group_name(self, function_name=None): """Returns the log group matching the current lambda function being parsed.""" if function_name: - return f'/aws/lambda/{function_name}' - return f'/aws/lambda/{self.aws.lambdaf.name}' + return f'/_aws/lambda/{function_name}' + return f'/_aws/lambda/{self._aws.lambdaf.name}' def _get_log_group_name_arg(self, function_name=None): return {'logGroupName' : self.get_log_group_name(function_name)} def _is_end_line(self, line): - return line.startswith('REPORT') and self.aws.cloudwatch.request_id in line + return line.startswith('REPORT') and self._aws.cloudwatch.request_id in line def _is_start_line(self, line): - return line.startswith('START') and self.aws.cloudwatch.request_id in line + return line.startswith('START') and self._aws.cloudwatch.request_id in line def _parse_logs_with_requestid(self, function_logs): parsed_msg = "" @@ -63,11 +63,11 @@ def _parse_logs_with_requestid(self, function_logs): def create_log_group(self): """Creates a CloudWatch Log Group.""" creation_args = self._get_log_group_name_arg() - creation_args['tags'] = self.aws.tags + creation_args['tags'] = self._aws.tags response = self.client.create_log_group(**creation_args) # Set retention policy into the log group retention_args = self._get_log_group_name_arg() - retention_args['retentionInDays'] = self.aws.cloudwatch.log_retention_policy_in_days + retention_args['retentionInDays'] = self._aws.cloudwatch.log_retention_policy_in_days self.client.set_log_retention_policy(**retention_args) return response @@ -80,10 +80,10 @@ def get_aws_log(self): function_logs = "" try: kwargs = self._get_log_group_name_arg() - if hasattr(self.aws.cloudwatch, "log_stream_name"): - kwargs["logStreamNames"] = [self.aws.cloudwatch.log_stream_name] + if hasattr(self._aws.cloudwatch, "log_stream_name"): + kwargs["logStreamNames"] = [self._aws.cloudwatch.log_stream_name] function_logs = _parse_events_in_message(self.client.get_log_events(**kwargs)) - if hasattr(self.aws.cloudwatch, "request_id") and self.aws.cloudwatch.request_id: + if hasattr(self._aws.cloudwatch, "request_id") and self._aws.cloudwatch.request_id: function_logs = self._parse_logs_with_requestid(function_logs) except ClientError as cerr: logger.warning("Error getting the function logs: %s" % cerr) @@ -95,7 +95,7 @@ def get_batch_job_log(self, jobs_info): if jobs_info: job = jobs_info[0] batch_logs += f"Batch job status: {job.get('status', '')}\n" - kwargs = {'logGroupName': "/aws/batch/job"} + kwargs = {'logGroupName': "/_aws/batch/job"} if job.get("status", "") == "SUCCEEDED": kwargs['logStreamNames'] = [job.get("container", {}).get("logStreamName", "")] batch_events = self.client.get_log_events(**kwargs) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 1f89306b..9b2332c7 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -58,8 +58,10 @@ def _get_iam_client(aws_properties: Dict): return IAM(aws_properties) -def _get_lambda_client(aws_properties: Dict, supervisor_version: Dict): - return Lambda(aws_properties, supervisor_version) +def _get_lambda_client(aws_properties: Dict = None): + if not aws_properties: + aws_properties = {} + return Lambda(aws_properties) def _get_api_gateway_client(aws_properties: Dict): @@ -69,6 +71,9 @@ def _get_api_gateway_client(aws_properties: Dict): def _get_s3_client(aws_properties: Dict): return S3(aws_properties) +def _get_resource_groups_client(aws_properties: Dict): + return ResourceGroups(aws_properties) + ############################################ ### ADD EXTRA PROPERTIES ### ############################################ @@ -98,20 +103,27 @@ def _add_handler(function): def _add_output(scar_properties, function): - function['lambda']['output'] = response_parser.OutputType.PLAIN_TEXT + function['lambda']['output'] = response_parser.OutputType.PLAIN_TEXT.value if scar_properties.get("json", False): - function['lambda']['output'] = response_parser.OutputType.JSON + function['lambda']['output'] = response_parser.OutputType.JSON.value # Override json ouput if both of them are defined if scar_properties.get("verbose", False): - function['lambda']['output'] = response_parser.OutputType.VERBOSE + function['lambda']['output'] = response_parser.OutputType.VERBOSE.value if scar_properties.get("output_file", False): - function['lambda']['output'] = response_parser.OutputType.BINARY + function['lambda']['output'] = response_parser.OutputType.BINARY.value function['lambda']['output_file'] = scar_properties.get("output_file") def _add_config_file_path(scar_properties, function): if scar_properties.get("conf_file", False): function['lambda']['config_path'] = os.path.dirname(scar_properties.get("conf_file")) + # Update the path of the files based on the path of the yaml (if any) + if function['lambda'].get('init_script', False): + function['lambda']['init_script'] = FileUtils.join_paths(function['lambda']['config_path'], + function['lambda']['init_script']) + if function['lambda'].get('image_file', False): + function['lambda']['image_file'] = FileUtils.join_paths(function['lambda']['config_path'], + function['lambda']['image_file']) ############################################ ### AWS CONTROLLER ### @@ -151,26 +163,41 @@ def resource_groups(self): resource_groups = ResourceGroups(self.aws_properties) return resource_groups + def __init__(self, func_call): + self.raw_args = FileUtils.load_config_file() + self.validate_arguments(self.raw_args) + self.aws_functions = self.raw_args.get('functions', {}).get('aws', {}) + self.storages = self.raw_args.get('storages', {}) + self.scar = self.raw_args.get('scar', {}) + _add_extra_aws_properties(self.scar, self.aws_functions) + # Call the user's command + getattr(self, func_call)() + + @AWSValidator.validate() + @excp.exception(logger) + def validate_arguments(self, merged_args: Dict) -> None: + pass + ############################################ ### AWS COMMANDS ### ############################################ @excp.exception(logger) def init(self) -> None: - supervisor_version = self.scar.get('supervisor_version', 'latest') + # supervisor_version = self.scar.get('supervisor_version', 'latest') for function in self.aws_functions: - lambda_client = _get_lambda_client(function, supervisor_version) + lambda_client = _get_lambda_client(function) if lambda_client.find_function(): raise excp.FunctionExistsError(function_name=function.get('lambda', {}).get('name', '')) # We have to create the gateway before creating the function - self._create_api_gateway(function) + #self._create_api_gateway(function) self._create_lambda_function(function, lambda_client) - self._create_log_group() - self._create_s3_buckets() + #self._create_log_group() + #self._create_s3_buckets() # The api_gateway permissions are added after the function is created - self._add_api_gateway_permissions() - self._create_batch_environment() - self._preheat_function() + #self._add_api_gateway_permissions() + #self._create_batch_environment() + #self._preheat_function() @excp.exception(logger) def invoke(self): @@ -211,11 +238,27 @@ def ls(self): @excp.exception(logger) def rm(self): - if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: - self._delete_all_resources() +# function_info = _get_lambda_client(self.aws_functions[0]).get_function_info(self.aws_functions[0]['lambda']['name']) +# self._delete_resources(function_info) + if self.scar.get('all', False): + "Delete all functions" + elif len(self.aws_functions) > 1: + "Please select the function to delete with -n" else: - function_info = self.aws_lambda.get_function_info(self.aws_properties.lambdaf.name) - self._delete_resources(function_info) + "Delete selected function" +# function_info = _get_lambda_client(self.aws_functions[0]).get_function_info(self.aws_properties.lambdaf.name) +# self._delete_resources(function_info) + + + function = self.aws_functions[0] + lambda_client = _get_lambda_client(function) + if not lambda_client.find_function(function['lambda']['name']): + raise excp.FunctionNotFoundError(function_name=function['lambda']['name']) + #self._delete_resources(function_info) +# if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: +# self._delete_all_resources() +# else: + @excp.exception(logger) def log(self): @@ -232,18 +275,19 @@ def put(self): def get(self): self.download_file_or_folder_from_s3() - @AWSValidator.validate() - @excp.exception(logger) - def parse_arguments(self, merged_args: Dict) -> None: - self.raw_args = merged_args - self.aws_functions = merged_args.get('functions', {}).get('aws', {}) - self.storages = merged_args.get('storages', {}) - self.scar = merged_args.get('scar', {}) - _add_extra_aws_properties(self.scar, self.aws_functions) + +############################################ +### ------------ ### +############################################ def _get_all_functions(self): - arn_list = self.resource_groups.get_resource_arn_list(self.iam.get_user_name_or_id()) - return self.aws_lambda.get_all_functions(arn_list) + # There can be several functions defined + # We check all and merge their arns to list them + functions_arn = set() + for function in self.aws_functions: + iam_info = _get_iam_client(function).get_user_name_or_id() + functions_arn.union(set(_get_resource_groups_client(function).get_resource_arn_list(iam_info))) + return _get_lambda_client().get_all_functions(functions_arn) def _get_batch_logs(self) -> str: logs = "" @@ -253,7 +297,6 @@ def _get_batch_logs(self) -> str: logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - @excp.exception(logger) def _create_log_group(self): response = self.cloudwatch_logs.create_log_group() @@ -353,7 +396,7 @@ def _create_api_gateway(self, function: Dict): @excp.exception(logger) def _create_lambda_function(self, function: Dict, lambda_client: Lambda) -> None: - response = lambda_client.create_function(function) + response = lambda_client.create_function() response_parser.parse_lambda_function_creation_response(response, function, lambda_client.get_access_key()) @@ -368,19 +411,19 @@ def _delete_all_resources(self): def _delete_resources(self, function_info): function_name = function_info['FunctionName'] - if not self.aws_lambda.find_function(function_name): - raise excp.FunctionNotFoundError(function_name=function_name) - # Delete associated api - self._delete_api_gateway(function_info['Environment']['Variables']) - # Delete associated log - self._delete_logs(function_name) - # Delete associated notifications - self._delete_bucket_notifications(function_info['FunctionArn'], - function_info['Environment']['Variables']) +# if not self.aws_lambda.find_function(function_name): +# raise excp.FunctionNotFoundError(function_name=function_name) +# # Delete associated api +# self._delete_api_gateway(function_info['Environment']['Variables']) +# # Delete associated log +# self._delete_logs(function_name) +# # Delete associated notifications +# self._delete_bucket_notifications(function_info['FunctionArn'], +# function_info['Environment']['Variables']) # Delete function self._delete_lambda_function(function_name) - # Delete resources batch - self._delete_batch_resources(function_name) +# # Delete resources batch +# self._delete_batch_resources(function_name) def _delete_api_gateway(self, function_env_vars): api_gateway_id = function_env_vars.get('API_GATEWAY_ID') diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 11c9bad8..eaad3e3c 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -13,105 +13,111 @@ # limitations under the License. """Module with methods and classes to create the function deployment package.""" +from typing import Dict from zipfile import ZipFile from scar.providers.aws.udocker import Udocker from scar.providers.aws.validators import AWSValidator from scar.exceptions import exception import scar.logger as logger from scar.http.request import get_file -from scar.utils import FileUtils, lazy_property, GitHubUtils, \ +from scar.utils import FileUtils, GitHubUtils, \ GITHUB_USER, GITHUB_SUPERVISOR_PROJECT - -_INIT_SCRIPT_NAME = "init_script.sh" - +def _get_udocker_client(aws: Dict, tmp_payload_folder_path: str, supervisor_zip_path: str) -> Udocker: + return Udocker(aws, tmp_payload_folder_path, supervisor_zip_path) class FunctionPackager(): """Class to manage the deployment package creation.""" - @lazy_property - def udocker(self): - """Udocker client""" - udocker = Udocker(self.aws, self.scar_tmp_function_folder_path, self._supervisor_zip_path) - return udocker - - def __init__(self, aws_properties): - self.aws = aws_properties -# self.scar_tmp_function_folder = FileUtils.create_tmp_dir() -# self.scar_tmp_function_folder_path = self.scar_tmp_function_folder.name -# self._supervisor_zip_path = FileUtils.join_paths(self.aws.lambdaf.tmp_folder_path, 'faas.zip') - self.package_args = {} + def __init__(self, aws_properties: Dict): + self._aws = aws_properties + # Temporal folder to store the supervisor and udocker files + self._tmp_payload_folder = FileUtils.create_tmp_dir() + # Path where the supervisor is downloaded + self._supervisor_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), 'faas.zip') @exception(logger) - def create_zip(self, tmp_folder): + def create_zip(self, lambda_payload_path: str) -> None: """Creates the lambda function deployment package.""" - zip_path = FileUtils.join_paths(tmp_folder, 'faas.zip') - self._download_faas_supervisor_zip(zip_path) - self._extract_handler_code(tmp_folder, zip_path) + self._download_faas_supervisor_zip() + self._extract_handler_code() + self._copy_function_configuration() self._manage_udocker_images() self._add_init_script() self._add_extra_payload() - self._zip_scar_folder() + self._zip_scar_folder(lambda_payload_path) self._check_code_size() - return zip_path - def _download_faas_supervisor_zip(self, zip_path) -> None: + def _download_faas_supervisor_zip(self) -> None: supervisor_zip_url = GitHubUtils.get_source_code_url( GITHUB_USER, GITHUB_SUPERVISOR_PROJECT, - self.aws.get('lambda').get('supervisor').get('version')) - with open(zip_path, "wb") as thezip: + self._aws.get('lambda').get('supervisor').get('version')) + with open(self._supervisor_zip_path, "wb") as thezip: thezip.write(get_file(supervisor_zip_url)) - def _extract_handler_code(self, tmp_folder, zip_path) -> None: - function_handler_dest = FileUtils.join_paths(tmp_folder, - f"{self.aws.get('lambda').get('name')}.py") + def _extract_handler_code(self) -> None: + function_handler_dest = FileUtils.join_paths(self._tmp_payload_folder.name, f"{self._aws.get('lambda').get('name')}.py") file_path = "" - with ZipFile(zip_path) as thezip: + with ZipFile(self._supervisor_zip_path) as thezip: for file in thezip.namelist(): if file.endswith("function_handler.py"): - file_path = FileUtils.join_paths(tmp_folder, file) - thezip.extract(file, tmp_folder) + file_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), file) + # Extracts the complete folder structure and the file (cannot avoid) + thezip.extract(file, FileUtils.get_tmp_dir()) break - FileUtils.copy_file(file_path, function_handler_dest) + if file_path: + # Copy only the handler to the payload folder + FileUtils.copy_file(file_path, function_handler_dest) + + def _copy_function_configuration(self): + cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") + raw_cfg_file = FileUtils.load_config_file() + function_cfg = {"storages" : raw_cfg_file.get('storages', {})} + function_cfg.update(self._aws['lambda']) + FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): - if hasattr(self.aws.lambdaf, "image") and \ - hasattr(self.aws, "s3") and \ - hasattr(self.aws.s3, "deployment_bucket"): - self.udocker.download_udocker_image() - if hasattr(self.aws.lambdaf, "image_file"): - if hasattr(self.aws, "config_path"): - self.aws.lambdaf.image_file = FileUtils.join_paths(self.aws.config_path, - self.aws.lambdaf.image_file) - self.udocker.prepare_udocker_image() + if self._aws.get('lambda').get('deployment').get('bucket', False) and \ + self._aws.get('lambda').get('container').get('image', False): + _get_udocker_client(self._aws, self._tmp_payload_folder.name, self._supervisor_zip_path).download_udocker_image() + if self._aws.get('lambda').get('image_file'): + _get_udocker_client(self._aws, self._tmp_payload_folder.name, self._supervisor_zip_path).prepare_udocker_image() + del(self._aws['lambda']['image_file']) - def _add_init_script(self): - if hasattr(self.aws.lambdaf, "init_script"): - if hasattr(self.aws, "config_path"): - self.aws.lambdaf.init_script = FileUtils.join_paths(self.aws.config_path, - self.aws.lambdaf.init_script) - FileUtils.copy_file(self.aws.lambdaf.init_script, - FileUtils.join_paths(self.scar_tmp_function_folder_path, _INIT_SCRIPT_NAME)) - self.aws.lambdaf.environment['Variables']['INIT_SCRIPT_PATH'] = \ - f"/var/task/{_INIT_SCRIPT_NAME}" + def _add_init_script(self) -> None: + """Copy the init script defined by the user to the payload folder.""" + if self._aws.get('lambda').get('init_script', False): + init_script_path = self._aws.get('lambda').get('init_script') + FileUtils.copy_file(init_script_path, + FileUtils.join_paths(self._tmp_payload_folder.name, + FileUtils.get_file_name(init_script_path))) + del(self._aws['lambda']['init_script']) - def _add_extra_payload(self): - if hasattr(self.aws.lambdaf, "extra_payload"): - logger.info("Adding extra payload from {0}".format(self.aws.lambdaf.extra_payload)) - FileUtils.copy_dir(self.aws.lambdaf.extra_payload, self.scar_tmp_function_folder_path) - self.aws.lambdaf.environment['Variables']['EXTRA_PAYLOAD'] = "/var/task" + def _add_extra_payload(self) -> None: + if self._aws.get('lambda').get('extra_payload', False): + payload_path = self._aws.get('lambda').get('extra_payload') + logger.info(f"Adding extra payload '{payload_path}'") + if FileUtils.is_file(payload_path): + FileUtils.copy_file(self._aws.get('lambda').get('extra_payload'), + self._tmp_payload_folder.name) + else: + FileUtils.copy_dir(self._aws.get('lambda').get('extra_payload'), + self._tmp_payload_folder.name) + del(self._aws['lambda']['extra_payload']) - def _zip_scar_folder(self): - FileUtils.zip_folder(self.aws.lambdaf.zip_file_path, - self.scar_tmp_function_folder_path, + def _zip_scar_folder(self, lambda_payload_path: str) -> None: + """Zips the tmp folder with all the function's files and + save it in the expected path of the payload.""" + FileUtils.zip_folder(lambda_payload_path, + self._tmp_payload_folder.name, "Creating function package") def _check_code_size(self): # Check if the code size fits within the AWS limits - if hasattr(self.aws, "s3") and hasattr(self.aws.s3, "deployment_bucket"): - AWSValidator.validate_s3_code_size(self.scar_tmp_function_folder_path, - self.aws.lambdaf.max_s3_payload_size) + if self._aws.get('lambda').get('deployment').get('bucket', False): + AWSValidator.validate_s3_code_size(self._tmp_payload_folder.name, + self._aws.get('lambda').get('deployment').get('max_s3_payload_size')) else: - AWSValidator.validate_function_code_size(self.scar_tmp_function_folder_path, - self.aws.lambdaf.max_payload_size) + AWSValidator.validate_function_code_size(self._tmp_payload_folder.name, + self._aws.get('lambda').get('deployment').get('max_payload_size')) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 2a50dbae..914bdb84 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -49,21 +49,21 @@ def _get_s3_client(aws_properties: Dict) -> S3: class Lambda(GenericClient): def __init__(self, aws_properties: Dict) -> None: - super().__init__(aws_properties.get('lambda')) - self.aws = aws_properties + super().__init__(aws_properties.get('lambda', {})) + self._aws = aws_properties self.function = aws_properties.get('lambda', {}) self.tmp_folder = FileUtils.create_tmp_dir() - self.zip_file_path = FileUtils.join_paths(self.tmp_folder.name, 'function.zip') + self.zip_payload_path = FileUtils.join_paths(self.tmp_folder.name, 'function.zip') def _get_creations_args(self): return {'FunctionName': self.function.get('name'), 'Runtime': self.function.get('runtime'), - 'Role': self.aws.get('iam').get('role'), + 'Role': self._aws.get('iam').get('role'), 'Handler': self.function.get('handler'), 'Code': self._get_function_code(), 'Environment': self.function.get('environment'), 'Description': self.function.get('description'), - 'Timeout': self.function.get('time'), + 'Timeout': self.function.get('timeout'), 'MemorySize': self.function.get('memory'), 'Tags': self.function.get('tags'), 'Layers': self.function.get('layers')} @@ -93,24 +93,24 @@ def _manage_supervisor_layer(self): def _get_function_code(self): # Zip all the files and folders needed code = {} - FunctionPackager(self.aws).create_zip(self.tmp_folder.name) - if self.function.get('deployment_bucket', False): + FunctionPackager(self._aws).create_zip(self.zip_payload_path) + if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" - self._get_s3_client().upload_file(file_path=self.zip_file_path, + self._get_s3_client().upload_file(file_path=self.zip_payload_path, file_key=file_key) code = {"S3Bucket": self.function.get('deployment_bucket'), "S3Key": file_key} else: - code = {"ZipFile": FileUtils.read_file(self.zip_file_path, mode="rb")} + code = {"ZipFile": FileUtils.read_file(self.zip_payload_path, mode="rb")} return code def delete_function(self, function_name): return self.client.delete_function(function_name) def link_function_and_input_bucket(self): - kwargs = {'FunctionName' : self.aws.lambdaf.name, + kwargs = {'FunctionName' : self._aws.lambdaf.name, 'Principal' : "s3.amazonaws.com", - 'SourceArn' : 'arn:aws:s3:::{0}'.format(self.aws.s3.input_bucket)} + 'SourceArn' : 'arn:_aws:s3:::{0}'.format(self._aws.s3.input_bucket)} self.client.add_invocation_permission(**kwargs) def preheat_function(self): @@ -127,7 +127,7 @@ def launch_request_response_event(self, s3_event): return self._launch_s3_event(s3_event) def _launch_s3_event(self, s3_event): - self.aws.lambdaf.payload = s3_event + self._aws.lambdaf.payload = s3_event logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") return self.launch_lambda_instance() @@ -147,62 +147,62 @@ def _launch_concurrent_lambda_invocations(self, s3_event_list): def launch_lambda_instance(self): response = self._invoke_lambda_function() response_args = {'Response' : response, - 'FunctionName' : self.aws.lambdaf.name, - 'OutputType' : self.aws.output, - 'IsAsynchronous' : self.aws.lambdaf.asynchronous} - if hasattr(self.aws, "output_file"): - response_args['OutputFile'] = self.aws.output_file + 'FunctionName' : self._aws.lambdaf.name, + 'OutputType' : self._aws.output, + 'IsAsynchronous' : self._aws.lambdaf.asynchronous} + if hasattr(self._aws, "output_file"): + response_args['OutputFile'] = self._aws.output_file response_parser.parse_invocation_response(**response_args) def _get_invocation_payload(self): # Default payload - payload = self.aws.lambdaf.payload if hasattr(self.aws.lambdaf, 'payload') else {} + payload = self._aws.lambdaf.payload if hasattr(self._aws.lambdaf, 'payload') else {} if not payload: # Check for defined run script - if hasattr(self.aws.lambdaf, "run_script"): - script_path = self.aws.lambdaf.run_script - if hasattr(self.aws, "config_path"): - script_path = FileUtils.join_paths(self.aws.config_path, script_path) + if hasattr(self._aws.lambdaf, "run_script"): + script_path = self._aws.lambdaf.run_script + if hasattr(self._aws, "config_path"): + script_path = FileUtils.join_paths(self._aws.config_path, script_path) # We first code to base64 in bytes and then decode those bytes to allow the json lib to parse the data # https://stackoverflow.com/questions/37225035/serialize-in-json-a-base64-encoded-data#37239382 payload = { "script" : StrUtils.bytes_to_base64str(FileUtils.read_file(script_path, 'rb')) } # Check for defined commands # This overrides any other function payload - if hasattr(self.aws.lambdaf, "c_args"): - payload = {"cmd_args" : json.dumps(self.aws.lambdaf.c_args)} + if hasattr(self._aws.lambdaf, "c_args"): + payload = {"cmd_args" : json.dumps(self._aws.lambdaf.c_args)} return json.dumps(payload) def _invoke_lambda_function(self): - invoke_args = {'FunctionName' : self.aws.lambdaf.name, - 'InvocationType' : self.aws.lambdaf.invocation_type, - 'LogType' : self.aws.lambdaf.log_type, + invoke_args = {'FunctionName' : self._aws.lambdaf.name, + 'InvocationType' : self._aws.lambdaf.invocation_type, + 'LogType' : self._aws.lambdaf.log_type, 'Payload' : self._get_invocation_payload()} return self.client.invoke_function(**invoke_args) def set_asynchronous_call_parameters(self): - self.aws.lambdaf.update_properties(**self.asynchronous_call_parameters) + self._aws.lambdaf.update_properties(**self.asynchronous_call_parameters) def _set_request_response_call_parameters(self): - self.aws.lambdaf.update_properties(**self.request_response_call_parameters) + self._aws.lambdaf.update_properties(**self.request_response_call_parameters) def _update_environment_variables(self, function_info, update_args): # To update the environment variables we need to retrieve the # variables defined in lambda and update them with the new values - env_vars = self.aws.lambdaf.environment - if hasattr(self.aws.lambdaf, "environment_variables"): - for env_var in self.aws.lambdaf.environment_variables: + env_vars = self._aws.lambdaf.environment + if hasattr(self._aws.lambdaf, "environment_variables"): + for env_var in self._aws.lambdaf.environment_variables: key_val = env_var.split("=") # Add an specific prefix to be able to find the variables defined by the user env_vars['Variables']['CONT_VAR_{0}'.format(key_val[0])] = key_val[1] - if hasattr(self.aws.lambdaf, "timeout_threshold"): - env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) - if hasattr(self.aws.lambdaf, "log_level"): - env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level + if hasattr(self._aws.lambdaf, "timeout_threshold"): + env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self._aws.lambdaf.timeout_threshold) + if hasattr(self._aws.lambdaf, "log_level"): + env_vars['Variables']['LOG_LEVEL'] = self._aws.lambdaf.log_level function_info['Environment']['Variables'].update(env_vars['Variables']) update_args['Environment'] = function_info['Environment'] def _update_supervisor_layer(self, function_info, update_args): - if hasattr(self.aws.lambdaf, "supervisor_layer"): + if hasattr(self._aws.lambdaf, "supervisor_layer"): # Set supervisor layer Arn function_layers = [self.layers.get_latest_supervisor_layer_arn()] # Add the rest of layers (if exist) @@ -214,12 +214,12 @@ def update_function_configuration(self, function_info=None): if not function_info: function_info = self.get_function_info() update_args = {'FunctionName' : function_info['FunctionName'] } -# if hasattr(self.aws.lambdaf, "memory"): -# update_args['MemorySize'] = self.aws.lambdaf.memory +# if hasattr(self._aws.lambdaf, "memory"): +# update_args['MemorySize'] = self._aws.lambdaf.memory # else: # update_args['MemorySize'] = function_info['MemorySize'] -# if hasattr(self.aws.lambdaf, "time"): -# update_args['Timeout'] = self.aws.lambdaf.time +# if hasattr(self._aws.lambdaf, "time"): +# update_args['Timeout'] = self._aws.lambdaf.time # else: # update_args['Timeout'] = function_info['Timeout'] self._update_environment_variables(function_info, update_args) @@ -237,7 +237,7 @@ def get_all_functions(self, arn_list): print (f"Error getting function info by arn: {cerr}") def get_function_info(self, function_name_or_arn=None): - name_arn = function_name_or_arn if function_name_or_arn else self.aws.lambdaf.name + name_arn = function_name_or_arn if function_name_or_arn else self._aws.lambdaf.name return self.client.get_function_info(name_arn) @excp.exception(logger) @@ -255,17 +255,17 @@ def find_function(self, function_name_or_arn=None): raise def add_invocation_permission_from_api_gateway(self): - kwargs = {'FunctionName' : self.aws.lambdaf.name, + kwargs = {'FunctionName' : self._aws.lambdaf.name, 'Principal' : 'apigateway.amazonaws.com', - 'SourceArn' : 'arn:aws:execute-api:{0}:{1}:{2}/*'.format(self.aws.region, - self.aws.account_id, - self.aws.api_gateway.id)} + 'SourceArn' : 'arn:_aws:execute-api:{0}:{1}:{2}/*'.format(self._aws.region, + self._aws.account_id, + self._aws.api_gateway.id)} # Add Testing permission self.client.add_invocation_permission(**kwargs) # Add Invocation permission - kwargs['SourceArn'] = 'arn:aws:execute-api:{0}:{1}:{2}/scar/ANY'.format(self.aws.region, - self.aws.account_id, - self.aws.api_gateway.id) + kwargs['SourceArn'] = 'arn:_aws:execute-api:{0}:{1}:{2}/scar/ANY'.format(self._aws.region, + self._aws.account_id, + self._aws.api_gateway.id) self.client.add_invocation_permission(**kwargs) def get_api_gateway_id(self): @@ -275,23 +275,23 @@ def get_api_gateway_id(self): def _get_api_gateway_url(self): api_id = self.get_api_gateway_id() if not api_id: - raise excp.ApiEndpointNotFoundError(self.aws.lambdaf.name) - return f'https://{api_id}.execute-api.{self.aws.region}.amazonaws.com/scar/launch' + raise excp.ApiEndpointNotFoundError(self._aws.lambdaf.name) + return f'https://{api_id}.execute-api.{self._aws.region}.amazonaws.com/scar/launch' def call_http_endpoint(self): invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} - if hasattr(self.aws, "api_gateway"): + if hasattr(self._aws, "api_gateway"): self._set_invoke_args(invoke_args) return request.call_http_endpoint(self._get_api_gateway_url(), **invoke_args) def _set_invoke_args(self, invoke_args): - if hasattr(self.aws.api_gateway, "data_binary"): - invoke_args['data'] = self._get_b64encoded_binary_data(self.aws.api_gateway.data_binary) + if hasattr(self._aws.api_gateway, "data_binary"): + invoke_args['data'] = self._get_b64encoded_binary_data(self._aws.api_gateway.data_binary) invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} - if hasattr(self.aws.api_gateway, "parameters"): - invoke_args['params'] = self._parse_http_parameters(self.aws.api_gateway.parameters) - if hasattr(self.aws.api_gateway, "json_data"): - invoke_args['data'] = self._parse_http_parameters(self.aws.api_gateway.json_data) + if hasattr(self._aws.api_gateway, "parameters"): + invoke_args['params'] = self._parse_http_parameters(self._aws.api_gateway.parameters) + if hasattr(self._aws.api_gateway, "json_data"): + invoke_args['data'] = self._parse_http_parameters(self._aws.api_gateway.json_data) invoke_args['headers'] = {'Content-Type': 'application/json'} def _parse_http_parameters(self, parameters): diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index 95c3fcfe..60d6cfc3 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -113,6 +113,7 @@ def __init__(self, function: Dict, lambda_client: LambdaClient) -> None: self.function = function self.lambda_client = lambda_client self.layer_name = self.function.get('supervisor').get('layer_name') + self.supervisor_version = self.function.get('supervisor').get('version') def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: return {'LayerName' : self.layer_name, @@ -151,6 +152,8 @@ def check_faas_supervisor_layer(self): If the layer exists and it's not updated, updates the layer.""" # Get the layer information layer_info = self.layer.get_latest_layer_info(self.layer_name) + import pprint + pprint.pprint(layer_info) # Compare supervisor versions if layer_info and 'Description' in layer_info: # If the supervisor layer version is lower than the passed version, diff --git a/scar/providers/aws/resourcegroups.py b/scar/providers/aws/resourcegroups.py index 26945832..a29eef9f 100644 --- a/scar/providers/aws/resourcegroups.py +++ b/scar/providers/aws/resourcegroups.py @@ -22,6 +22,9 @@ class ResourceGroups(GenericClient): """Class to manage AWS Resource Groups""" + def __init__(self, aws_properties) -> None: + super().__init__(aws_properties.get('lambda')) + def get_resource_arn_list(self, iam_user_id: str, resource_type: str = 'lambda') -> List: """Returns a list of ARNs filtered by the resource_type passed and the tags created by scar.""" diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index 58645990..16df355e 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -28,7 +28,7 @@ class OutputType(Enum): def parse_http_response(response, function_name, asynch, output_type, output_file): if response.ok: - if output_type == OutputType.BINARY: + if output_type == OutputType.BINARY.value: with open(output_file, "wb") as out: out.write(StrUtils.decode_base64(response.text)) text_message = f"Output saved in file '{output_file}'" @@ -53,18 +53,18 @@ def parse_http_response(response, function_name, asynch, output_type, output_fil def _print_generic_response(response, output_type, aws_output, text_message=None, json_output=None, verbose_output=None, output_file=None): - if output_type == OutputType.BINARY: + if output_type == OutputType.BINARY.value: with open(output_file, "wb") as out: out.write(StrUtils.decode_base64(response['Payload']['body'])) - elif output_type == OutputType.PLAIN_TEXT: + elif output_type == OutputType.PLAIN_TEXT.value: output = text_message logger.info(output) else: - if output_type == OutputType.JSON: + if output_type == OutputType.JSON.value: output = json_output if json_output else {aws_output : {'RequestId' : response['ResponseMetadata']['RequestId'], 'HTTPStatusCode' : response['ResponseMetadata']['HTTPStatusCode']}} - elif output_type == OutputType.VERBOSE: + elif output_type == OutputType.VERBOSE.value: output = verbose_output if verbose_output else {aws_output : response} logger.info_json(output) @@ -113,7 +113,7 @@ def parse_ls_response(lambda_functions, output_type): aws_output = 'Functions' result = [] text_message = "" - if output_type == OutputType.VERBOSE: + if output_type == OutputType.VERBOSE.value: result = lambda_functions else: for lambdaf in lambda_functions: diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index 1eb6b9d8..e583b577 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -25,18 +25,18 @@ class S3(GenericClient): def __init__(self, aws_properties): super().__init__(aws_properties.get('lambda')) - if hasattr(self.aws, 's3'): - if type(self.aws.s3) is dict: - self.aws.s3 = S3Properties(self.aws.s3) + if hasattr(self._aws, 's3'): + if type(self._aws.s3) is dict: + self._aws.s3 = S3Properties(self._aws.s3) self._initialize_properties() def _initialize_properties(self): - if not hasattr(self.aws.s3, "input_folder"): - self.aws.s3.input_folder = '' - if hasattr(self.aws.lambdaf, "name"): - self.aws.s3.input_folder = "{0}/input/".format(self.aws.lambdaf.name) - elif not self.aws.s3.input_folder.endswith("/"): - self.aws.s3.input_folder = "{0}/".format(self.aws.s3.input_folder) + if not hasattr(self._aws.s3, "input_folder"): + self._aws.s3.input_folder = '' + if hasattr(self._aws.lambdaf, "name"): + self._aws.s3.input_folder = "{0}/input/".format(self._aws.lambdaf.name) + elif not self._aws.s3.input_folder.endswith("/"): + self._aws.s3.input_folder = "{0}/".format(self._aws.s3.input_folder) @excp.exception(logger) def create_bucket(self, bucket_name): @@ -44,28 +44,28 @@ def create_bucket(self, bucket_name): self.client.create_bucket(bucket_name) def create_output_bucket(self): - self.create_bucket(self.aws.s3.output_bucket) + self.create_bucket(self._aws.s3.output_bucket) @excp.exception(logger) def add_bucket_folder(self): - if self.aws.s3.input_folder: - self.upload_file(folder_name=self.aws.s3.input_folder) + if self._aws.s3.input_folder: + self.upload_file(folder_name=self._aws.s3.input_folder) def create_input_bucket(self, create_input_folder=False): - self.create_bucket(self.aws.s3.input_bucket) + self.create_bucket(self._aws.s3.input_bucket) if create_input_folder: self.add_bucket_folder() def set_input_bucket_notification(self): # First check that the function doesn't have other configurations - bucket_conf = self.client.get_notification_configuration(self.aws.s3.input_bucket) + bucket_conf = self.client.get_notification_configuration(self._aws.s3.input_bucket) trigger_conf = self.get_trigger_configuration() lambda_conf = [trigger_conf] if "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] lambda_conf.append(trigger_conf) notification = { "LambdaFunctionConfigurations": lambda_conf } - self.client.put_notification_configuration(self.aws.s3.input_bucket, notification) + self.client.put_notification_configuration(self._aws.s3.input_bucket, notification) def delete_bucket_notification(self, bucket_name, function_arn): bucket_conf = self.client.get_notification_configuration(bucket_name) @@ -77,9 +77,9 @@ def delete_bucket_notification(self, bucket_name, function_arn): logger.info("Bucket notifications successfully deleted") def get_trigger_configuration(self): - return {"LambdaFunctionArn": self.aws.lambdaf.arn, + return {"LambdaFunctionArn": self._aws.lambdaf.arn, "Events": [ "s3:ObjectCreated:*" ], - "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": self.aws.s3.input_folder }]}} + "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": self._aws.s3.input_folder }]}} } def get_file_key(self, folder_name=None, file_path=None, file_key=None): @@ -96,7 +96,7 @@ def get_file_key(self, folder_name=None, file_path=None, file_key=None): @excp.exception(logger) def upload_file(self, folder_name=None, file_path=None, file_key=None): - kwargs = {'Bucket' : self.aws.s3.input_bucket} + kwargs = {'Bucket' : self._aws.s3.input_bucket} kwargs['Key'] = self.get_file_key(folder_name, file_path, file_key) if file_path: try: @@ -111,19 +111,19 @@ def upload_file(self, folder_name=None, file_path=None, file_key=None): @excp.exception(logger) def get_bucket_file_list(self): - bucket_name = self.aws.s3.input_bucket + bucket_name = self._aws.s3.input_bucket if self.client.find_bucket(bucket_name): kwargs = {"Bucket" : bucket_name} - if hasattr(self.aws.s3, "input_folder") and self.aws.s3.input_folder: - kwargs["Prefix"] = self.aws.s3.input_folder + if hasattr(self._aws.s3, "input_folder") and self._aws.s3.input_folder: + kwargs["Prefix"] = self._aws.s3.input_folder return self.client.list_files(**kwargs) else: raise excp.BucketNotFoundError(bucket_name=bucket_name) def get_s3_event(self, s3_file_key): - return {"Records": [{"eventSource": "aws:s3", - "s3" : {"bucket" : {"name": self.aws.s3.input_bucket, - "arn": f'arn:aws:s3:::{self.aws.s3.input_bucket}'}, + return {"Records": [{"eventSource": "_aws:s3", + "s3" : {"bucket" : {"name": self._aws.s3.input_bucket, + "arn": f'arn:_aws:s3:::{self._aws.s3.input_bucket}'}, "object" : {"key": s3_file_key}}}]} def get_s3_event_list(self, s3_file_keys): diff --git a/scar/providers/aws/udocker.py b/scar/providers/aws/udocker.py index 9a1671bc..f7cd69d3 100644 --- a/scar/providers/aws/udocker.py +++ b/scar/providers/aws/udocker.py @@ -29,76 +29,70 @@ def _extract_udocker_zip(supervisor_zip_path) -> None: class Udocker(): - def __init__(self, aws_properties, function_tmp_folder, supervisor_zip_path): - self.aws = aws_properties - self.function_tmp_folder = function_tmp_folder - self.udocker_dir = FileUtils.join_paths(self.function_tmp_folder, "udocker") - self.udocker_dir_orig = "" - self._initialize_udocker(supervisor_zip_path) - - def _initialize_udocker(self, supervisor_zip_path): - self.udocker_code = FileUtils.join_paths(self.udocker_dir, "udocker.py") - self.udocker_exec = ['python3', self.udocker_code] + def __init__(self, aws_properties: str, tmp_payload_folder_path: str, supervisor_zip_path: str): + self._aws = aws_properties + self._tmp_payload_folder_path = tmp_payload_folder_path + self._udocker_dir = FileUtils.join_paths(self._tmp_payload_folder_path, "udocker") + self._udocker_dir_orig = "" + self._udocker_code = FileUtils.join_paths(self._udocker_dir, "udocker.py") + self._udocker_exec = ['python3', self._udocker_code] self._install_udocker(supervisor_zip_path) - def _install_udocker(self, supervisor_zip_path): + def _install_udocker(self, supervisor_zip_path: str) -> None: udocker_zip_path = _extract_udocker_zip(supervisor_zip_path) with ZipFile(udocker_zip_path) as thezip: - thezip.extractall(self.function_tmp_folder) + thezip.extractall(self._tmp_payload_folder_path) - def save_tmp_udocker_env(self): + def _save_tmp_udocker_env(self): # Avoid override global variables if SysUtils.is_variable_in_environment("UDOCKER_DIR"): - self.udocker_dir_orig = SysUtils.get_environment_variable("UDOCKER_DIR") + self._udocker_dir_orig = SysUtils.get_environment_variable("UDOCKER_DIR") # Set temporal global vars - SysUtils.set_environment_variable("UDOCKER_DIR", self.udocker_dir) + SysUtils.set_environment_variable("UDOCKER_DIR", self._udocker_dir) - def restore_udocker_env(self): - if self.udocker_dir_orig: - SysUtils.set_environment_variable("UDOCKER_DIR", self.udocker_dir_orig) + def _restore_udocker_env(self): + if self._udocker_dir_orig: + SysUtils.set_environment_variable("UDOCKER_DIR", self._udocker_dir_orig) else: SysUtils.delete_environment_variable("UDOCKER_DIR") def _set_udocker_local_registry(self): - self.aws.lambdaf.environment['Variables']['UDOCKER_REPOS'] = '/var/task/udocker/repos/' - self.aws.lambdaf.environment['Variables']['UDOCKER_LAYERS'] = '/var/task/udocker/layers/' + self._aws['lambda']['environment']['Variables']['UDOCKER_REPOS'] = '/var/task/udocker/repos/' + self._aws['lambda']['environment']['Variables']['UDOCKER_LAYERS'] = '/var/task/udocker/layers/' def _create_udocker_container(self): """Check if the container fits in the limits of the deployment.""" - if hasattr(self.aws, "s3") and hasattr(self.aws.s3, "deployment_bucket"): - self._validate_container_size(self.aws.lambdaf.max_s3_payload_size) + if self._aws.get('lambda').get('deployment').get('bucket', False): + self._validate_container_size(self._aws.get('lambda').get('deployment').get('max_s3_payload_size')) else: - self._validate_container_size(self.aws.lambdaf.max_payload_size) + self._validate_container_size(self._aws.get('lambda').get('deployment').get('max_payload_size')) def _validate_container_size(self, max_payload_size): - if FileUtils.get_tree_size(self.udocker_dir) < (max_payload_size / 2): - ucmd = self.udocker_exec + ["create", "--name=lambda_cont", self.aws.lambdaf.image] + if FileUtils.get_tree_size(self._udocker_dir) < (max_payload_size / 2): + ucmd = self._udocker_exec + ["create", "--name=lambda_cont", self._aws.get('lambda').get('container').get('image')] SysUtils.execute_command_with_msg(ucmd, cli_msg="Creating container structure") + self._aws['lambda']['environment']['Variables']['UDOCKER_CONTAINERS'] = '/var/task/udocker/containers/' - elif FileUtils.get_tree_size(self.udocker_dir) > max_payload_size: - FileUtils.delete_folder(FileUtils.join_paths(self.udocker_dir, "containers")) - - else: - self.aws.lambdaf.environment['Variables']['UDOCKER_LAYERS'] = \ - '/var/task/udocker/containers/' + elif FileUtils.get_tree_size(self._udocker_dir) > max_payload_size: + FileUtils.delete_folder(FileUtils.join_paths(self._udocker_dir, "containers")) + def download_udocker_image(self): - self.save_tmp_udocker_env() - SysUtils.execute_command_with_msg(self.udocker_exec + ["pull", self.aws.lambdaf.image], + self._save_tmp_udocker_env() + SysUtils.execute_command_with_msg(self._udocker_exec + ["pull", self._aws.get('lambda').get('container').get('image')], cli_msg="Downloading container image") self._create_udocker_container() self._set_udocker_local_registry() - self.restore_udocker_env() + self._restore_udocker_env() def prepare_udocker_image(self): - self.save_tmp_udocker_env() + self._save_tmp_udocker_env() image_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), "udocker_image.tar.gz") - FileUtils.copy_file(self.aws.lambdaf.image_file, image_path) - cmd_out = SysUtils.execute_command_with_msg(self.udocker_exec + ["load", "-i", image_path], + FileUtils.copy_file(self._aws.get('lambda').get('image_file'), image_path) + cmd_out = SysUtils.execute_command_with_msg(self._udocker_exec + ["load", "-i", image_path], cli_msg="Loading image file") # Get the image name from the command output - self.aws.lambdaf.image = cmd_out.split('\n')[1] + self._aws['lambda']['container']['image'] = cmd_out.split('\n')[1] self._create_udocker_container() - self.aws.lambdaf.environment['Variables']['IMAGE_ID'] = self.aws.lambdaf.image self._set_udocker_local_registry() - self.restore_udocker_env() + self._restore_udocker_env() diff --git a/scar/scarcli.py b/scar/scarcli.py index a866ef55..308b9e19 100755 --- a/scar/scarcli.py +++ b/scar/scarcli.py @@ -17,7 +17,6 @@ import sys sys.path.append('.') -from scar.cmdtemplate import Commands from scar.parser.cfgfile import ConfigFileParser from scar.parser.cli import CommandParser from scar.providers.aws.controller import AWS @@ -27,69 +26,37 @@ import scar.logger as logger -class ScarCLI(Commands): - - def __init__(self): - self.cloud_provider = AWS() - - def init(self): - self.cloud_provider.init() - - def invoke(self): - self.cloud_provider.invoke() - - def run(self): - self.cloud_provider.run() - - def update(self): - self.cloud_provider.update() - - def ls(self): - self.cloud_provider.ls() - - def rm(self): - self.cloud_provider.rm() - - def log(self): - self.cloud_provider.log() - - def put(self): - self.cloud_provider.put() - - def get(self): - self.cloud_provider.get() - - @excp.exception(logger) - def parse_arguments(self): - """ - Merge the scar.conf parameters, the cmd parameters and the yaml - file parameters in a single dictionary. - - The precedence of parameters is CMD >> YAML >> SCAR.CONF - That is, the CMD parameter will override any other configuration, - and the YAML parameters will override the SCAR.CONF settings - """ - config_args = ConfigFileParser().get_properties() - cmd_args = CommandParser(self).parse_arguments() - if 'conf_file' in cmd_args['scar'] and cmd_args['scar']['conf_file']: - yaml_args = FileUtils.load_yaml(cmd_args['scar']['conf_file']) - # YAML >> SCAR.CONF - merged_args = fdl.merge_conf(config_args, yaml_args) - merged_args = fdl.merge_cmd_yaml(cmd_args, merged_args) - else: - # CMD >> SCAR.CONF - merged_args = fdl.merge_conf(config_args, cmd_args) - import pprint - pprint.pprint(merged_args) - self.cloud_provider.parse_arguments(merged_args) - #merged_args.get('scar').get('func')() - pprint.pprint(merged_args) - +@excp.exception(logger) +def parse_arguments(): + """ + Merge the scar.conf parameters, the cmd parameters and the yaml + file parameters in a single dictionary. + + The precedence of parameters is CMD >> YAML >> SCAR.CONF + That is, the CMD parameter will override any other configuration, + and the YAML parameters will override the SCAR.CONF settings + """ + config_args = ConfigFileParser().get_properties() + func_call, cmd_args = CommandParser().parse_arguments() + if 'conf_file' in cmd_args['scar'] and cmd_args['scar']['conf_file']: + yaml_args = FileUtils.load_yaml(cmd_args['scar']['conf_file']) + # YAML >> SCAR.CONF + merged_args = fdl.merge_conf(config_args, yaml_args) + merged_args = fdl.merge_cmd_yaml(cmd_args, merged_args) + else: + # CMD >> SCAR.CONF + merged_args = fdl.merge_conf(config_args, cmd_args) + #self.cloud_provider.parse_arguments(merged_args) + FileUtils.create_config_file(merged_args) + return func_call def main(): logger.init_execution_trace() try: - ScarCLI().parse_arguments() + func_call = parse_arguments() + # Default provider + # If more providers, analyze the arguments and build the required one + AWS(func_call) logger.end_execution_trace() except Exception as excp: print(excp) diff --git a/scar/utils.py b/scar/utils.py index 80ce09a1..5656e5ec 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -33,7 +33,7 @@ GITHUB_USER = 'grycap' GITHUB_SUPERVISOR_PROJECT = 'faas-supervisor' - +COMMANDS = ['scar-config'] def lazy_property(func): # Skipped type hinting: https://github.com/python/mypy/issues/3157 @@ -301,7 +301,25 @@ def load_yaml(file_path: str) -> Dict: return yaml.safe_load(cfg_file) else: raise YamlFileNotFoundError(file_path=file_path) - + + @staticmethod + def write_yaml(file_path: str, content: Dict) -> None: + with open(file_path, 'w') as cfg_file: + yaml.safe_dump(content, cfg_file) + + @staticmethod + def create_config_file(cfg_args): + cfg_path = FileUtils.join_paths(SysUtils.get_user_home_path(), ".scar", "scar_tmp.yaml") + os.environ['SCAR_TMP_CFG'] = cfg_path + FileUtils.write_yaml(cfg_path, cfg_args) + + @staticmethod + def load_config_file(): + return FileUtils.load_yaml(os.environ['SCAR_TMP_CFG']) + + @staticmethod + def get_file_name(file_path: str) -> str: + return os.path.basename(file_path) class StrUtils: """Common methods for string management.""" diff --git a/video-process.yaml b/video-process.yaml index f7ffc07d..804177a8 100644 --- a/video-process.yaml +++ b/video-process.yaml @@ -7,10 +7,10 @@ functions: execution_mode: batch log_level: debug input: - - storage_name: s3-bucket + - storage_provider: s3 path: scar-ffmpeg output: - - storage_name: s3-bucket + - storage_provider: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output - lambda: name: scar-lambda-darknet @@ -19,13 +19,13 @@ functions: log_level: debug init_script: yolo-sample-object-detection.sh input: - - storage_name: s3-bucket + - storage_provider: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/video-output output: - - storage_name: s3-bucket + - storage_provider: s3 path: scar-ffmpeg/scar-batch-ffmpeg-split/image-output -storages: +storage_providers: s3: - name: s3-bucket minio: From 70545d3e6352dedccdeb66f7cbd10700d170309d Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 20 Nov 2019 14:47:15 +0100 Subject: [PATCH 04/41] WIP - Add delete lambda function --- scar/providers/aws/controller.py | 41 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 9b2332c7..a911ce98 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -54,24 +54,24 @@ def _get_owner(function): ############################################ -def _get_iam_client(aws_properties: Dict): +def _get_iam_client(aws_properties: Dict) -> IAM: return IAM(aws_properties) -def _get_lambda_client(aws_properties: Dict = None): +def _get_lambda_client(aws_properties: Dict = None) -> Lambda: if not aws_properties: aws_properties = {} return Lambda(aws_properties) -def _get_api_gateway_client(aws_properties: Dict): +def _get_api_gateway_client(aws_properties: Dict) -> APIGateway: return APIGateway(aws_properties) -def _get_s3_client(aws_properties: Dict): +def _get_s3_client(aws_properties: Dict) -> S3: return S3(aws_properties) -def _get_resource_groups_client(aws_properties: Dict): +def _get_resource_groups_client(aws_properties: Dict) -> ResourceGroups: return ResourceGroups(aws_properties) ############################################ @@ -243,17 +243,18 @@ def rm(self): if self.scar.get('all', False): "Delete all functions" elif len(self.aws_functions) > 1: - "Please select the function to delete with -n" + "Please select the function to delete" else: "Delete selected function" -# function_info = _get_lambda_client(self.aws_functions[0]).get_function_info(self.aws_properties.lambdaf.name) -# self._delete_resources(function_info) - + function = self.aws_functions[0] + lambda_client = _get_lambda_client(function) + function_info = lambda_client.get_function_info(function.get('lambda').get('name')) + self._delete_resources(lambda_client, function_info) - function = self.aws_functions[0] - lambda_client = _get_lambda_client(function) - if not lambda_client.find_function(function['lambda']['name']): - raise excp.FunctionNotFoundError(function_name=function['lambda']['name']) +# function = self.aws_functions[0] +# lambda_client = _get_lambda_client(function) +# if not lambda_client.find_function(function['lambda']['name']): +# raise excp.FunctionNotFoundError(function_name=function['lambda']['name']) #self._delete_resources(function_info) # if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: # self._delete_all_resources() @@ -409,10 +410,10 @@ def _delete_all_resources(self): for function_info in self._get_all_functions(): self._delete_resources(function_info) - def _delete_resources(self, function_info): + def _delete_resources(self, lambda_client: Lambda, function_info: Dict) -> None: function_name = function_info['FunctionName'] -# if not self.aws_lambda.find_function(function_name): -# raise excp.FunctionNotFoundError(function_name=function_name) + if not lambda_client.find_function(function_name): + raise excp.FunctionNotFoundError(function_name=function_name) # # Delete associated api # self._delete_api_gateway(function_info['Environment']['Variables']) # # Delete associated log @@ -421,7 +422,7 @@ def _delete_resources(self, function_info): # self._delete_bucket_notifications(function_info['FunctionArn'], # function_info['Environment']['Variables']) # Delete function - self._delete_lambda_function(function_name) + self._delete_lambda_function(lambda_client, function_name) # # Delete resources batch # self._delete_batch_resources(function_name) @@ -447,10 +448,10 @@ def _delete_bucket_notifications(self, function_arn, function_env_vars): input_bucket_name = input_path.split("/", 1)[0] self.aws_s3.delete_bucket_notification(input_bucket_name, function_arn) - def _delete_lambda_function(self, function_name): - response = self.aws_lambda.delete_function(function_name) + def _delete_lambda_function(self, lambda_client: Lambda, function_name: str): + response = lambda_client.delete_function(function_name) response_parser.parse_delete_function_response(response, function_name, - self.aws_properties.output) + lambda_client.function.get('output')) def _delete_batch_resources(self, function_name): if self.batch.exist_compute_environments(function_name): From 84af2e3511ec37fd55e1be98f35839d4d15e571f Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 20 Nov 2019 15:11:44 +0100 Subject: [PATCH 05/41] WIP - Add first version of new ls command --- scar/providers/aws/controller.py | 96 ++++++++++++++++++---------- scar/providers/aws/lambdafunction.py | 22 +++---- 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index a911ce98..38bda846 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -124,6 +124,9 @@ def _add_config_file_path(scar_properties, function): if function['lambda'].get('image_file', False): function['lambda']['image_file'] = FileUtils.join_paths(function['lambda']['config_path'], function['lambda']['image_file']) + if function['lambda'].get('run_script', False): + function['lambda']['run_script'] = FileUtils.join_paths(function['lambda']['config_path'], + function['lambda']['run_script']) ############################################ ### AWS CONTROLLER ### @@ -201,43 +204,58 @@ def init(self) -> None: @excp.exception(logger) def invoke(self): - self._update_local_function_properties() - response = self.aws_lambda.call_http_endpoint() - response_parser.parse_http_response(response, - self.aws_properties.lambdaf.name, - self.aws_properties.lambdaf.asynchronous, - self.aws_properties.output, - getattr(self.scar, "output_file", "")) + 'TODO' +# self._update_local_function_properties() +# response = self.aws_lambda.call_http_endpoint() +# response_parser.parse_http_response(response, +# self.aws_properties.lambdaf.name, +# self.aws_properties.lambdaf.asynchronous, +# self.aws_properties.output, +# getattr(self.scar, "output_file", "")) @excp.exception(logger) def run(self): - if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): - self._process_input_bucket_calls() + if len(self.aws_functions) > 1: + logger.info("Not allowed yet") else: - if self.aws_lambda.is_asynchronous(): - self.aws_lambda.set_asynchronous_call_parameters() - self.aws_lambda.launch_lambda_instance() + lambda_client = _get_lambda_client(self.aws_functions[0]) + if lambda_client.is_asynchronous(): + lambda_client.set_asynchronous_call_parameters() + lambda_client.launch_lambda_instance() + 'TODO FINISH' +# if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): +# self._process_input_bucket_calls() +# else: +# if self.aws_lambda.is_asynchronous(): +# self.aws_lambda.set_asynchronous_call_parameters() +# self.aws_lambda.launch_lambda_instance() @excp.exception(logger) def update(self): - if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: - self._update_all_functions(self._get_all_functions()) - else: - self.aws_lambda.update_function_configuration() + 'TODO' +# if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: +# self._update_all_functions(self._get_all_functions()) +# else: +# self.aws_lambda.update_function_configuration() @excp.exception(logger) def ls(self): - if self.storages: - file_list = _get_s3_client(self.aws_functions).get_bucket_file_list() - for file_info in file_list: - print(file_info) - else: - lambda_functions = self._get_all_functions() - response_parser.parse_ls_response(lambda_functions, - self.aws_properties.output) + lambda_functions = self._get_all_functions() + response_parser.parse_ls_response(lambda_functions, + self.aws_functions[0].get('lambda').get('output')) + 'TODO FINISH' +# if self.storages: +# file_list = _get_s3_client(self.aws_functions).get_bucket_file_list() +# for file_info in file_list: +# print(file_info) +# else: +# lambda_functions = self._get_all_functions() +# response_parser.parse_ls_response(lambda_functions, +# self.aws_properties.output) @excp.exception(logger) def rm(self): + 'TODO FINISH' # function_info = _get_lambda_client(self.aws_functions[0]).get_function_info(self.aws_functions[0]['lambda']['name']) # self._delete_resources(function_info) if self.scar.get('all', False): @@ -263,18 +281,21 @@ def rm(self): @excp.exception(logger) def log(self): - aws_log = self.cloudwatch_logs.get_aws_log() - batch_logs = self._get_batch_logs() - aws_log += batch_logs if batch_logs else "" - print(aws_log) + 'TODO' +# aws_log = self.cloudwatch_logs.get_aws_log() +# batch_logs = self._get_batch_logs() +# aws_log += batch_logs if batch_logs else "" +# print(aws_log) @excp.exception(logger) def put(self): - self.upload_file_or_folder_to_s3() + 'TODO' +# self.upload_file_or_folder_to_s3() @excp.exception(logger) def get(self): - self.download_file_or_folder_from_s3() + 'TODO' +# self.download_file_or_folder_from_s3() ############################################ @@ -284,11 +305,16 @@ def get(self): def _get_all_functions(self): # There can be several functions defined # We check all and merge their arns to list them - functions_arn = set() - for function in self.aws_functions: - iam_info = _get_iam_client(function).get_user_name_or_id() - functions_arn.union(set(_get_resource_groups_client(function).get_resource_arn_list(iam_info))) - return _get_lambda_client().get_all_functions(functions_arn) + +# for function in self.aws_functions: +# iam_info = _get_iam_client(function).get_user_name_or_id() +# functions_arn.union(set(_get_resource_groups_client(function).get_resource_arn_list(iam_info))) +# return _get_lambda_client(self.aws_functions[0]).get_all_functions(functions_arn) + function = self.aws_functions[0] + arn_list = _get_resource_groups_client(function).get_resource_arn_list(_get_iam_client(function).get_user_name_or_id()) + return _get_lambda_client(function).get_all_functions(arn_list) + + def _get_batch_logs(self) -> str: logs = "" diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 914bdb84..3f896325 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -156,34 +156,32 @@ def launch_lambda_instance(self): def _get_invocation_payload(self): # Default payload - payload = self._aws.lambdaf.payload if hasattr(self._aws.lambdaf, 'payload') else {} + payload = self.function.get('payload', {}) if not payload: # Check for defined run script - if hasattr(self._aws.lambdaf, "run_script"): - script_path = self._aws.lambdaf.run_script - if hasattr(self._aws, "config_path"): - script_path = FileUtils.join_paths(self._aws.config_path, script_path) + if self.function.get("run_script", False): + script_path = self.function.get("run_script") # We first code to base64 in bytes and then decode those bytes to allow the json lib to parse the data # https://stackoverflow.com/questions/37225035/serialize-in-json-a-base64-encoded-data#37239382 payload = { "script" : StrUtils.bytes_to_base64str(FileUtils.read_file(script_path, 'rb')) } # Check for defined commands # This overrides any other function payload - if hasattr(self._aws.lambdaf, "c_args"): - payload = {"cmd_args" : json.dumps(self._aws.lambdaf.c_args)} + if self.function.get("c_args", False): + payload = {"cmd_args" : json.dumps(self.function.get("c_args"))} return json.dumps(payload) def _invoke_lambda_function(self): - invoke_args = {'FunctionName' : self._aws.lambdaf.name, - 'InvocationType' : self._aws.lambdaf.invocation_type, - 'LogType' : self._aws.lambdaf.log_type, + invoke_args = {'FunctionName' : self.function.get('name'), + 'InvocationType' : self.function.get('invocation_type'), + 'LogType' : self.function.get('log_type'), 'Payload' : self._get_invocation_payload()} return self.client.invoke_function(**invoke_args) def set_asynchronous_call_parameters(self): - self._aws.lambdaf.update_properties(**self.asynchronous_call_parameters) + self.function.update(ASYNCHRONOUS_CALL) def _set_request_response_call_parameters(self): - self._aws.lambdaf.update_properties(**self.request_response_call_parameters) + self.function.update(REQUEST_RESPONSE_CALL) def _update_environment_variables(self, function_info, update_args): # To update the environment variables we need to retrieve the From b14f3ba61465f0f3853402a4e1bf5c5682a5ce4f Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 20 Nov 2019 16:14:20 +0100 Subject: [PATCH 06/41] WIP - Add create and delete cloudwatch logs --- scar/providers/aws/cloudwatchlogs.py | 43 ++++++------ scar/providers/aws/controller.py | 99 +++++++++++----------------- scar/providers/aws/lambdalayers.py | 2 - 3 files changed, 64 insertions(+), 80 deletions(-) diff --git a/scar/providers/aws/cloudwatchlogs.py b/scar/providers/aws/cloudwatchlogs.py index ec5c3ef1..0da148e6 100644 --- a/scar/providers/aws/cloudwatchlogs.py +++ b/scar/providers/aws/cloudwatchlogs.py @@ -14,7 +14,7 @@ """Module with classes and methods to manage the CloudWatch Log functionalities at high level.""" -from typing import List +from typing import List, Dict from botocore.exceptions import ClientError from scar.providers.aws import GenericClient import scar.logger as logger @@ -28,24 +28,29 @@ def _parse_events_in_message(log_events: List) -> str: class CloudWatchLogs(GenericClient): """Manages the AWS CloudWatch Logs functionality""" + + def __init__(self, aws_properties: Dict): + super().__init__(aws_properties.get('cloudwatch')) + self._aws = aws_properties + self.cloudwatch = aws_properties.get('cloudwatch') - def get_log_group_name(self, function_name=None): + def get_log_group_name(self, function_name: str=None) -> str: """Returns the log group matching the current lambda function being parsed.""" if function_name: - return f'/_aws/lambda/{function_name}' - return f'/_aws/lambda/{self._aws.lambdaf.name}' + return f'/aws/lambda/{function_name}' + return f'/aws/lambda/{self._aws.get("lambda").get("name")}' - def _get_log_group_name_arg(self, function_name=None): + def _get_log_group_name_arg(self, function_name: str=None) -> Dict: return {'logGroupName' : self.get_log_group_name(function_name)} - def _is_end_line(self, line): - return line.startswith('REPORT') and self._aws.cloudwatch.request_id in line + def _is_end_line(self, line: str) -> bool: + return line.startswith('REPORT') and self.cloudwatch.get('request_id') in line - def _is_start_line(self, line): - return line.startswith('START') and self._aws.cloudwatch.request_id in line + def _is_start_line(self, line: str) -> bool: + return line.startswith('START') and self.cloudwatch.get('request_id') in line - def _parse_logs_with_requestid(self, function_logs): + def _parse_logs_with_requestid(self, function_logs: str) -> str: parsed_msg = "" if function_logs: in_req_id_logs = False @@ -60,36 +65,36 @@ def _parse_logs_with_requestid(self, function_logs): parsed_msg += f'{line}\n' return parsed_msg - def create_log_group(self): + def create_log_group(self) -> Dict: """Creates a CloudWatch Log Group.""" creation_args = self._get_log_group_name_arg() - creation_args['tags'] = self._aws.tags + creation_args['tags'] = self._aws.get('lambda').get('tags') response = self.client.create_log_group(**creation_args) # Set retention policy into the log group retention_args = self._get_log_group_name_arg() - retention_args['retentionInDays'] = self._aws.cloudwatch.log_retention_policy_in_days + retention_args['retentionInDays'] = self.cloudwatch.get('log_retention_policy_in_days') self.client.set_log_retention_policy(**retention_args) return response - def delete_log_group(self, log_group_name): + def delete_log_group(self, log_group_name: str) -> Dict: """Deletes a CloudWatch Log Group.""" return self.client.delete_log_group(log_group_name) - def get_aws_log(self): + def get_aws_log(self) -> str: """Returns Lambda logs for an specific lambda function.""" function_logs = "" try: kwargs = self._get_log_group_name_arg() - if hasattr(self._aws.cloudwatch, "log_stream_name"): - kwargs["logStreamNames"] = [self._aws.cloudwatch.log_stream_name] + if self.cloudwatch.get("log_stream_name", False): + kwargs["logStreamNames"] = [self.cloudwatch.get("log_stream_name")] function_logs = _parse_events_in_message(self.client.get_log_events(**kwargs)) - if hasattr(self._aws.cloudwatch, "request_id") and self._aws.cloudwatch.request_id: + if self.cloudwatch.get("request_id", False): function_logs = self._parse_logs_with_requestid(function_logs) except ClientError as cerr: logger.warning("Error getting the function logs: %s" % cerr) return function_logs - def get_batch_job_log(self, jobs_info): + def get_batch_job_log(self, jobs_info: List) -> str: """Returns Batch logs for an specific job.""" batch_logs = "" if jobs_info: diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 38bda846..4634e2ca 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -58,7 +58,7 @@ def _get_iam_client(aws_properties: Dict) -> IAM: return IAM(aws_properties) -def _get_lambda_client(aws_properties: Dict = None) -> Lambda: +def _get_lambda_client(aws_properties: Dict=None) -> Lambda: if not aws_properties: aws_properties = {} return Lambda(aws_properties) @@ -71,9 +71,14 @@ def _get_api_gateway_client(aws_properties: Dict) -> APIGateway: def _get_s3_client(aws_properties: Dict) -> S3: return S3(aws_properties) + def _get_resource_groups_client(aws_properties: Dict) -> ResourceGroups: return ResourceGroups(aws_properties) + +def _get_cloudwatch_logs_client(aws_properties: Dict) -> CloudWatchLogs: + return CloudWatchLogs(aws_properties) + ############################################ ### ADD EXTRA PROPERTIES ### ############################################ @@ -137,35 +142,12 @@ class AWS(Commands): """AWS controller. Used to manage all the AWS calls and functionalities.""" -# @lazy_property -# def aws_lambda(self): -# """It's called 'aws_lambda' because 'lambda' -# it's a restricted word in python.""" -# aws_lambda = Lambda(self.aws_properties, -# self.scar.supervisor_version) -# return aws_lambda - @lazy_property def batch(self): batch = Batch(self.aws_properties, self.scar.supervisor_version) return batch - @lazy_property - def cloudwatch_logs(self): - cloudwatch_logs = CloudWatchLogs(self.aws_properties) - return cloudwatch_logs - - @lazy_property - def api_gateway(self): - api_gateway = APIGateway(self.aws_properties) - return api_gateway - - @lazy_property - def resource_groups(self): - resource_groups = ResourceGroups(self.aws_properties) - return resource_groups - def __init__(self, func_call): self.raw_args = FileUtils.load_config_file() self.validate_arguments(self.raw_args) @@ -193,14 +175,14 @@ def init(self) -> None: if lambda_client.find_function(): raise excp.FunctionExistsError(function_name=function.get('lambda', {}).get('name', '')) # We have to create the gateway before creating the function - #self._create_api_gateway(function) + # self._create_api_gateway(function) self._create_lambda_function(function, lambda_client) - #self._create_log_group() - #self._create_s3_buckets() + self._create_log_group(function) + # self._create_s3_buckets() # The api_gateway permissions are added after the function is created - #self._add_api_gateway_permissions() - #self._create_batch_environment() - #self._preheat_function() + # self._add_api_gateway_permissions() + # self._create_batch_environment() + # self._preheat_function() @excp.exception(logger) def invoke(self): @@ -266,19 +248,20 @@ def rm(self): "Delete selected function" function = self.aws_functions[0] lambda_client = _get_lambda_client(function) - function_info = lambda_client.get_function_info(function.get('lambda').get('name')) - self._delete_resources(lambda_client, function_info) +# function_info = lambda_client.get_function_info(function.get('lambda').get('name')) + if not lambda_client.find_function(function.get('lambda').get('name')): + raise excp.FunctionNotFoundError(function_name=function.get('lambda').get('name')) + self._delete_resources(function) # function = self.aws_functions[0] # lambda_client = _get_lambda_client(function) # if not lambda_client.find_function(function['lambda']['name']): # raise excp.FunctionNotFoundError(function_name=function['lambda']['name']) - #self._delete_resources(function_info) + # self._delete_resources(function_info) # if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: # self._delete_all_resources() # else: - @excp.exception(logger) def log(self): 'TODO' @@ -297,7 +280,6 @@ def get(self): 'TODO' # self.download_file_or_folder_from_s3() - ############################################ ### ------------ ### ############################################ @@ -313,8 +295,6 @@ def _get_all_functions(self): function = self.aws_functions[0] arn_list = _get_resource_groups_client(function).get_resource_arn_list(_get_iam_client(function).get_user_name_or_id()) return _get_lambda_client(function).get_all_functions(arn_list) - - def _get_batch_logs(self) -> str: logs = "" @@ -324,13 +304,6 @@ def _get_batch_logs(self) -> str: logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - @excp.exception(logger) - def _create_log_group(self): - response = self.cloudwatch_logs.create_log_group() - response_parser.parse_log_group_creation_response(response, - self.cloudwatch_logs.get_log_group_name(), - self.aws_properties.output) - @excp.exception(logger) def _create_s3_buckets(self): if hasattr(self.aws_properties, "s3"): @@ -428,6 +401,13 @@ def _create_lambda_function(self, function: Dict, lambda_client: Lambda) -> None function, lambda_client.get_access_key()) + @excp.exception(logger) + def _create_log_group(self, function: Dict): + cloudwatch_logs = _get_cloudwatch_logs_client(function) + response = cloudwatch_logs.create_log_group() + response_parser.parse_log_group_creation_response(response, + cloudwatch_logs.get_log_group_name(), + function.get('lambda').get('output')) ############################################################################# ### Methods to delete AWS resources ### ############################################################################# @@ -436,19 +416,18 @@ def _delete_all_resources(self): for function_info in self._get_all_functions(): self._delete_resources(function_info) - def _delete_resources(self, lambda_client: Lambda, function_info: Dict) -> None: - function_name = function_info['FunctionName'] - if not lambda_client.find_function(function_name): - raise excp.FunctionNotFoundError(function_name=function_name) + def _delete_resources(self, function: Dict) -> None: +# function_name = function_info['FunctionName'] + # # Delete associated api # self._delete_api_gateway(function_info['Environment']['Variables']) -# # Delete associated log -# self._delete_logs(function_name) + # Delete associated log + self._delete_logs(function) # # Delete associated notifications # self._delete_bucket_notifications(function_info['FunctionArn'], # function_info['Environment']['Variables']) # Delete function - self._delete_lambda_function(lambda_client, function_name) + self._delete_lambda_function(function) # # Delete resources batch # self._delete_batch_resources(function_name) @@ -459,12 +438,13 @@ def _delete_api_gateway(self, function_env_vars): response_parser.parse_delete_api_response(response, api_gateway_id, self.aws_properties.output) - def _delete_logs(self, function_name): - log_group_name = self.cloudwatch_logs.get_log_group_name(function_name) - response = self.cloudwatch_logs.delete_log_group(log_group_name) + def _delete_logs(self, function: Dict): + cloudwatch_logs = _get_cloudwatch_logs_client(function) + log_group_name = cloudwatch_logs.get_log_group_name(function.get('lambda').get('name')) + response = cloudwatch_logs.delete_log_group(log_group_name) response_parser.parse_delete_log_response(response, log_group_name, - self.aws_properties.output) + function.get('lambda').get('output')) def _delete_bucket_notifications(self, function_arn, function_env_vars): s3_provider_id = _get_storage_provider_id('S3', function_env_vars) @@ -474,10 +454,11 @@ def _delete_bucket_notifications(self, function_arn, function_env_vars): input_bucket_name = input_path.split("/", 1)[0] self.aws_s3.delete_bucket_notification(input_bucket_name, function_arn) - def _delete_lambda_function(self, lambda_client: Lambda, function_name: str): - response = lambda_client.delete_function(function_name) - response_parser.parse_delete_function_response(response, function_name, - lambda_client.function.get('output')) + def _delete_lambda_function(self, function: Dict): + response = _get_lambda_client(function).delete_function(function.get('lambda').get('name')) + response_parser.parse_delete_function_response(response, + function.get('lambda').get('name'), + function.get('lambda').get('output')) def _delete_batch_resources(self, function_name): if self.batch.exist_compute_environments(function_name): diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index 60d6cfc3..fa4a7aac 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -152,8 +152,6 @@ def check_faas_supervisor_layer(self): If the layer exists and it's not updated, updates the layer.""" # Get the layer information layer_info = self.layer.get_latest_layer_info(self.layer_name) - import pprint - pprint.pprint(layer_info) # Compare supervisor versions if layer_info and 'Description' in layer_info: # If the supervisor layer version is lower than the passed version, From e3268c4bdf80188bb941143b2e57bb5c7a21cd5a Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 20 Nov 2019 18:06:32 +0100 Subject: [PATCH 07/41] WIP - Add creation and deletion of api gateway --- scar/parser/cfgfile.py | 8 ++- scar/providers/aws/apigateway.py | 37 ++++++------ scar/providers/aws/controller.py | 83 ++++++++++++++------------- scar/providers/aws/lambdafunction.py | 84 ++++++++++++++-------------- 4 files changed, 111 insertions(+), 101 deletions(-) diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index 4e8ad4b3..4042e5de 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -21,7 +21,7 @@ _DEFAULT_CFG = { "scar": { - "config_version": "1.0.5" + "config_version": "1.0.6" }, "aws": { "iam": {"boto_profile": "default", @@ -82,7 +82,11 @@ "method.request.header.X-Amz-Invocation-Type"} }, 'path_part': "{proxy+}", - 'stage_name': "scar" + 'stage_name': "scar", + # Used to add invocation permissions to lambda + 'service_id': 'apigateway.amazonaws.com', + 'source_arn_testing': 'arn:aws:execute-api:{api_region}:{account_id}:{api_id}/*', + 'source_arn_invocation': 'arn:aws:execute-api:{api_region}:{account_id}:{api_id}/{stage_name}/ANY' }, "cloudwatch": { "boto_profile": "default", diff --git a/scar/providers/aws/apigateway.py b/scar/providers/aws/apigateway.py index e6dd2360..25971df9 100644 --- a/scar/providers/aws/apigateway.py +++ b/scar/providers/aws/apigateway.py @@ -17,13 +17,14 @@ from scar.providers.aws import GenericClient import scar.logger as logger + class APIGateway(GenericClient): """Manage the calls to the ApiGateway client.""" def __init__(self, aws_properties: Dict): super().__init__(aws_properties.get('api_gateway', {})) - self._aws = aws_properties - self.api = self.aws_properties.get('api_gateway', {}) + self.aws = aws_properties + self.api = self.aws.get('api_gateway', {}) def _get_common_args(self) -> Dict: return {'restApiId' : self.api.get('id', ''), @@ -31,20 +32,24 @@ def _get_common_args(self) -> Dict: 'httpMethod' : self.api.get('http_method', '')} def _get_method_args(self) -> Dict: - return self._get_common_args().update(self.api.get('method', {})) + args = self._get_common_args() + args.update(self.api.get('method', {})) + return args def _get_integration_args(self) -> Dict: integration_args = self.api.get('integration', {}) uri_args = {'api_region': self.api.get('region', ''), - 'lambda_region': self._aws.get('lambda', {}).get('region', ''), - 'account_id': self._aws.get('iam', {}).get('account_id', ''), - 'function_name': self._aws.get('lambda', {}).get('name', '')} + 'lambda_region': self.aws.get('lambda', {}).get('region', ''), + 'account_id': self.aws.get('iam', {}).get('account_id', ''), + 'function_name': self.aws.get('lambda', {}).get('name', '')} integration_args['uri'] = integration_args['uri'].format(**uri_args) - return self._get_common_args().update(integration_args) + args = self._get_common_args() + args.update(integration_args) + return args def _get_resource_id(self) -> str: res_id = "" - resources_info = self.client.get_resources(self.api.get('id','')) + resources_info = self.client.get_resources(self.api.get('id', '')) for resource in resources_info['items']: if resource['path'] == '/': res_id = resource['id'] @@ -55,26 +60,26 @@ def _set_api_gateway_id(self, api_info: Dict) -> None: self.api['id'] = api_info.get('id', '') # We store the parameter in the lambda configuration that # is going to be uploaded to the Lambda service - self._aws['lambda']['api_gateway_id'] = api_info.get('id', '') - + self.aws['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_info.get('id', '') + def _set_resource_info_id(self, resource_info: Dict) -> None: - self.api['resource_id'] = resource_info.get('id', '') + self.api['resource_id'] = resource_info.get('id', '') def _get_endpoint(self) -> str: - endpoint_args = {'api_id': self.api.get('id',''), 'api_region': self.api.get('region','')} - return self.api.get('endpoint','').format(**endpoint_args) + endpoint_args = {'api_id': self.api.get('id', ''), 'api_region': self.api.get('region', '')} + return self.api.get('endpoint', '').format(**endpoint_args) def create_api_gateway(self) -> None: """Creates an Api Gateway endpoint.""" - api_info = self.client.create_rest_api(self.api.get('name','')) + api_info = self.client.create_rest_api(self.api.get('name', '')) self._set_api_gateway_id(api_info) - resource_info = self.client.create_resource(self.api.get('id',''), + resource_info = self.client.create_resource(self.api.get('id', ''), self._get_resource_id(), self.api.get('path_part', '')) self._set_resource_info_id(resource_info) self.client.create_method(**self._get_method_args()) self.client.set_integration(**self._get_integration_args()) - self.client.create_deployment(self.api.get('id',''), self.api.get('stage_name', '')) + self.client.create_deployment(self.api.get('id', ''), self.api.get('stage_name', '')) logger.info(f'API Gateway endpoint: {self._get_endpoint()}') def delete_api_gateway(self, api_gateway_id: str) -> None: diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 4634e2ca..c856d7ea 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -34,16 +34,16 @@ _ACCOUNT_ID_REGEX = r'\d{12}' -def _get_storage_provider_id(storage_provider: str, env_vars: Dict) -> str: - """Searches the storage provider id in the environment variables: - get_provider_id(S3, {'STORAGE_AUTH_S3_USER_41807' : 'scar'}) - returns -> 41807""" - res = "" - for env_key in env_vars.keys(): - if env_key.startswith(f'STORAGE_AUTH_{storage_provider}'): - res = env_key.split('_', 4)[-1] - break - return res +# def _get_storage_provider_id(storage_provider: str, env_vars: Dict) -> str: +# """Searches the storage provider id in the environment variables: +# get_provider_id(S3, {'STORAGE_AUTH_S3_USER_41807' : 'scar'}) +# returns -> 41807""" +# res = "" +# for env_key in env_vars.keys(): +# if env_key.startswith(f'STORAGE_AUTH_{storage_provider}'): +# res = env_key.split('_', 4)[-1] +# break +# return res def _get_owner(function): @@ -93,35 +93,35 @@ def _add_extra_aws_properties(scar: Dict, aws_functions: Dict) -> None: _add_config_file_path(scar, function) -def _add_tags(function): +def _add_tags(function: Dict): function['lambda']['tags'] = {"createdby": "scar", "owner": _get_owner(function)} -def _add_account_id(function): +def _add_account_id(function: Dict): function['iam']['account_id'] = StrUtils.find_expression(function['iam']['role'], _ACCOUNT_ID_REGEX) -def _add_handler(function): +def _add_handler(function: Dict): function['lambda']['handler'] = f"{function.get('lambda', {}).get('name', '')}.lambda_handler" -def _add_output(scar_properties, function): +def _add_output(scar_props: Dict, function: Dict): function['lambda']['output'] = response_parser.OutputType.PLAIN_TEXT.value - if scar_properties.get("json", False): + if scar_props.get("json", False): function['lambda']['output'] = response_parser.OutputType.JSON.value # Override json ouput if both of them are defined - if scar_properties.get("verbose", False): + if scar_props.get("verbose", False): function['lambda']['output'] = response_parser.OutputType.VERBOSE.value - if scar_properties.get("output_file", False): + if scar_props.get("output_file", False): function['lambda']['output'] = response_parser.OutputType.BINARY.value - function['lambda']['output_file'] = scar_properties.get("output_file") + function['lambda']['output_file'] = scar_props.get("output_file") -def _add_config_file_path(scar_properties, function): - if scar_properties.get("conf_file", False): - function['lambda']['config_path'] = os.path.dirname(scar_properties.get("conf_file")) +def _add_config_file_path(scar_props: Dict, function: Dict): + if scar_props.get("conf_file", False): + function['lambda']['config_path'] = os.path.dirname(scar_props.get("conf_file")) # Update the path of the files based on the path of the yaml (if any) if function['lambda'].get('init_script', False): function['lambda']['init_script'] = FileUtils.join_paths(function['lambda']['config_path'], @@ -175,12 +175,12 @@ def init(self) -> None: if lambda_client.find_function(): raise excp.FunctionExistsError(function_name=function.get('lambda', {}).get('name', '')) # We have to create the gateway before creating the function - # self._create_api_gateway(function) + self._create_api_gateway(function) self._create_lambda_function(function, lambda_client) self._create_log_group(function) # self._create_s3_buckets() # The api_gateway permissions are added after the function is created - # self._add_api_gateway_permissions() + self._add_api_gateway_permissions(function) # self._create_batch_environment() # self._preheat_function() @@ -197,13 +197,11 @@ def invoke(self): @excp.exception(logger) def run(self): - if len(self.aws_functions) > 1: - logger.info("Not allowed yet") - else: - lambda_client = _get_lambda_client(self.aws_functions[0]) - if lambda_client.is_asynchronous(): - lambda_client.set_asynchronous_call_parameters() - lambda_client.launch_lambda_instance() + function = self.aws_functions[0] + lambda_client = _get_lambda_client(function) + if lambda_client.is_asynchronous(): + lambda_client.set_asynchronous_call_parameters() + lambda_client.launch_lambda_instance() 'TODO FINISH' # if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): # self._process_input_bucket_calls() @@ -314,9 +312,9 @@ def _create_s3_buckets(self): if hasattr(self.aws_properties.s3, "output_bucket"): self.aws_s3.create_output_bucket() - def _add_api_gateway_permissions(self): - if hasattr(self.aws_properties, "api_gateway"): - self.aws_lambda.add_invocation_permission_from_api_gateway() + def _add_api_gateway_permissions(self, function: Dict): + if function.get("api_gateway", {}).get('name', False): + _get_lambda_client(function).add_invocation_permission_from_api_gateway() def _create_batch_environment(self): if self.aws_properties.execution_mode == "batch" or \ @@ -418,9 +416,8 @@ def _delete_all_resources(self): def _delete_resources(self, function: Dict) -> None: # function_name = function_info['FunctionName'] - -# # Delete associated api -# self._delete_api_gateway(function_info['Environment']['Variables']) + # Delete associated api + self._delete_api_gateway(function) # Delete associated log self._delete_logs(function) # # Delete associated notifications @@ -431,12 +428,14 @@ def _delete_resources(self, function: Dict) -> None: # # Delete resources batch # self._delete_batch_resources(function_name) - def _delete_api_gateway(self, function_env_vars): - api_gateway_id = function_env_vars.get('API_GATEWAY_ID') + def _delete_api_gateway(self, function): + lambda_client = _get_lambda_client(function) + api_gateway_id = lambda_client.get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') if api_gateway_id: - response = self.api_gateway.delete_api_gateway(api_gateway_id) - response_parser.parse_delete_api_response(response, api_gateway_id, - self.aws_properties.output) + response = _get_api_gateway_client(function).delete_api_gateway(api_gateway_id) + response_parser.parse_delete_api_response(response, + api_gateway_id, + function.get('lambda').get('output')) def _delete_logs(self, function: Dict): cloudwatch_logs = _get_cloudwatch_logs_client(function) @@ -447,7 +446,7 @@ def _delete_logs(self, function: Dict): function.get('lambda').get('output')) def _delete_bucket_notifications(self, function_arn, function_env_vars): - s3_provider_id = _get_storage_provider_id('S3', function_env_vars) + s3_provider_id = ""#_get_storage_provider_id('S3', function_env_vars) input_bucket_id = f'STORAGE_PATH_INPUT_{s3_provider_id}' if s3_provider_id else '' if input_bucket_id in function_env_vars: input_path = function_env_vars[input_bucket_id] diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 3f896325..ac4ff71f 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -50,7 +50,7 @@ class Lambda(GenericClient): def __init__(self, aws_properties: Dict) -> None: super().__init__(aws_properties.get('lambda', {})) - self._aws = aws_properties + self.aws = aws_properties self.function = aws_properties.get('lambda', {}) self.tmp_folder = FileUtils.create_tmp_dir() self.zip_payload_path = FileUtils.join_paths(self.tmp_folder.name, 'function.zip') @@ -58,7 +58,7 @@ def __init__(self, aws_properties: Dict) -> None: def _get_creations_args(self): return {'FunctionName': self.function.get('name'), 'Runtime': self.function.get('runtime'), - 'Role': self._aws.get('iam').get('role'), + 'Role': self.aws.get('iam').get('role'), 'Handler': self.function.get('handler'), 'Code': self._get_function_code(), 'Environment': self.function.get('environment'), @@ -93,7 +93,7 @@ def _manage_supervisor_layer(self): def _get_function_code(self): # Zip all the files and folders needed code = {} - FunctionPackager(self._aws).create_zip(self.zip_payload_path) + FunctionPackager(self.aws).create_zip(self.zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" self._get_s3_client().upload_file(file_path=self.zip_payload_path, @@ -108,9 +108,9 @@ def delete_function(self, function_name): return self.client.delete_function(function_name) def link_function_and_input_bucket(self): - kwargs = {'FunctionName' : self._aws.lambdaf.name, + kwargs = {'FunctionName' : self.aws.lambdaf.name, 'Principal' : "s3.amazonaws.com", - 'SourceArn' : 'arn:_aws:s3:::{0}'.format(self._aws.s3.input_bucket)} + 'SourceArn' : 'arn:aws:s3:::{0}'.format(self.aws.s3.input_bucket)} self.client.add_invocation_permission(**kwargs) def preheat_function(self): @@ -127,7 +127,7 @@ def launch_request_response_event(self, s3_event): return self._launch_s3_event(s3_event) def _launch_s3_event(self, s3_event): - self._aws.lambdaf.payload = s3_event + self.aws.lambdaf.payload = s3_event logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") return self.launch_lambda_instance() @@ -147,11 +147,11 @@ def _launch_concurrent_lambda_invocations(self, s3_event_list): def launch_lambda_instance(self): response = self._invoke_lambda_function() response_args = {'Response' : response, - 'FunctionName' : self._aws.lambdaf.name, - 'OutputType' : self._aws.output, - 'IsAsynchronous' : self._aws.lambdaf.asynchronous} - if hasattr(self._aws, "output_file"): - response_args['OutputFile'] = self._aws.output_file + 'FunctionName' : self.aws.lambdaf.name, + 'OutputType' : self.aws.output, + 'IsAsynchronous' : self.aws.lambdaf.asynchronous} + if hasattr(self.aws, "output_file"): + response_args['OutputFile'] = self.aws.output_file response_parser.parse_invocation_response(**response_args) def _get_invocation_payload(self): @@ -186,21 +186,21 @@ def _set_request_response_call_parameters(self): def _update_environment_variables(self, function_info, update_args): # To update the environment variables we need to retrieve the # variables defined in lambda and update them with the new values - env_vars = self._aws.lambdaf.environment - if hasattr(self._aws.lambdaf, "environment_variables"): - for env_var in self._aws.lambdaf.environment_variables: + env_vars = self.aws.lambdaf.environment + if hasattr(self.aws.lambdaf, "environment_variables"): + for env_var in self.aws.lambdaf.environment_variables: key_val = env_var.split("=") # Add an specific prefix to be able to find the variables defined by the user env_vars['Variables']['CONT_VAR_{0}'.format(key_val[0])] = key_val[1] - if hasattr(self._aws.lambdaf, "timeout_threshold"): - env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self._aws.lambdaf.timeout_threshold) - if hasattr(self._aws.lambdaf, "log_level"): - env_vars['Variables']['LOG_LEVEL'] = self._aws.lambdaf.log_level + if hasattr(self.aws.lambdaf, "timeout_threshold"): + env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) + if hasattr(self.aws.lambdaf, "log_level"): + env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level function_info['Environment']['Variables'].update(env_vars['Variables']) update_args['Environment'] = function_info['Environment'] def _update_supervisor_layer(self, function_info, update_args): - if hasattr(self._aws.lambdaf, "supervisor_layer"): + if hasattr(self.aws.lambdaf, "supervisor_layer"): # Set supervisor layer Arn function_layers = [self.layers.get_latest_supervisor_layer_arn()] # Add the rest of layers (if exist) @@ -212,12 +212,12 @@ def update_function_configuration(self, function_info=None): if not function_info: function_info = self.get_function_info() update_args = {'FunctionName' : function_info['FunctionName'] } -# if hasattr(self._aws.lambdaf, "memory"): -# update_args['MemorySize'] = self._aws.lambdaf.memory +# if hasattr(self.aws.lambdaf, "memory"): +# update_args['MemorySize'] = self.aws.lambdaf.memory # else: # update_args['MemorySize'] = function_info['MemorySize'] -# if hasattr(self._aws.lambdaf, "time"): -# update_args['Timeout'] = self._aws.lambdaf.time +# if hasattr(self.aws.lambdaf, "time"): +# update_args['Timeout'] = self.aws.lambdaf.time # else: # update_args['Timeout'] = function_info['Timeout'] self._update_environment_variables(function_info, update_args) @@ -235,7 +235,7 @@ def get_all_functions(self, arn_list): print (f"Error getting function info by arn: {cerr}") def get_function_info(self, function_name_or_arn=None): - name_arn = function_name_or_arn if function_name_or_arn else self._aws.lambdaf.name + name_arn = function_name_or_arn if function_name_or_arn else self.function.get('name') return self.client.get_function_info(name_arn) @excp.exception(logger) @@ -253,17 +253,19 @@ def find_function(self, function_name_or_arn=None): raise def add_invocation_permission_from_api_gateway(self): - kwargs = {'FunctionName' : self._aws.lambdaf.name, - 'Principal' : 'apigateway.amazonaws.com', - 'SourceArn' : 'arn:_aws:execute-api:{0}:{1}:{2}/*'.format(self._aws.region, - self._aws.account_id, - self._aws.api_gateway.id)} + api = self.aws.get('api_gateway') # Add Testing permission + kwargs = {'FunctionName': self.function.get('name'), + 'Principal': api.get('service_id'), + 'SourceArn': api.get('source_arn_testing').format(api_region=api.get('region'), + account_id=self.aws.get('iam').get('account_id'), + api_id=api.get('id'))} self.client.add_invocation_permission(**kwargs) # Add Invocation permission - kwargs['SourceArn'] = 'arn:_aws:execute-api:{0}:{1}:{2}/scar/ANY'.format(self._aws.region, - self._aws.account_id, - self._aws.api_gateway.id) + kwargs['SourceArn'] = api.get('source_arn_invocation').format(api_region=api.get('region'), + account_id=self.aws.get('iam').get('account_id'), + api_id=api.get('id'), + stage_name=api.get('stage_name')) self.client.add_invocation_permission(**kwargs) def get_api_gateway_id(self): @@ -273,23 +275,23 @@ def get_api_gateway_id(self): def _get_api_gateway_url(self): api_id = self.get_api_gateway_id() if not api_id: - raise excp.ApiEndpointNotFoundError(self._aws.lambdaf.name) - return f'https://{api_id}.execute-api.{self._aws.region}.amazonaws.com/scar/launch' + raise excp.ApiEndpointNotFoundError(self.aws.lambdaf.name) + return f'https://{api_id}.execute-api.{self.aws.region}.amazonaws.com/scar/launch' def call_http_endpoint(self): invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} - if hasattr(self._aws, "api_gateway"): + if hasattr(self.aws, "api_gateway"): self._set_invoke_args(invoke_args) return request.call_http_endpoint(self._get_api_gateway_url(), **invoke_args) def _set_invoke_args(self, invoke_args): - if hasattr(self._aws.api_gateway, "data_binary"): - invoke_args['data'] = self._get_b64encoded_binary_data(self._aws.api_gateway.data_binary) + if hasattr(self.aws.api_gateway, "data_binary"): + invoke_args['data'] = self._get_b64encoded_binary_data(self.aws.api_gateway.data_binary) invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} - if hasattr(self._aws.api_gateway, "parameters"): - invoke_args['params'] = self._parse_http_parameters(self._aws.api_gateway.parameters) - if hasattr(self._aws.api_gateway, "json_data"): - invoke_args['data'] = self._parse_http_parameters(self._aws.api_gateway.json_data) + if hasattr(self.aws.api_gateway, "parameters"): + invoke_args['params'] = self._parse_http_parameters(self.aws.api_gateway.parameters) + if hasattr(self.aws.api_gateway, "json_data"): + invoke_args['data'] = self._parse_http_parameters(self.aws.api_gateway.json_data) invoke_args['headers'] = {'Content-Type': 'application/json'} def _parse_http_parameters(self, parameters): From 58195df8e7d6e029569ab828a75aab334eecb1f4 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Thu, 21 Nov 2019 16:35:22 +0100 Subject: [PATCH 08/41] WIP - Add S3 bucket and folders creation --- scar/parser/cli/__init__.py | 7 +- scar/providers/aws/__init__.py | 15 +- scar/providers/aws/controller.py | 96 +++---- scar/providers/aws/lambdafunction.py | 35 ++- scar/providers/aws/properties.py | 382 +++++++++++++-------------- scar/providers/aws/response.py | 2 +- scar/providers/aws/s3.py | 87 +++--- 7 files changed, 312 insertions(+), 312 deletions(-) diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index 9aea1ed2..50cbef4f 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -120,14 +120,13 @@ def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: s3_args = DataTypesUtils.parse_arg_list(s3_arg_list, cmd_args) storage = {} if s3_args: - s3_id = StrUtils.get_random_uuid4_str() if 'deployment_bucket' in s3_args: aws_args['lambda']['deployment'] = {'bucket': s3_args['deployment_bucket']} if 'input_bucket' in s3_args: - aws_args['lambda']['input'] = [{'storage_name' : s3_id, 'path': s3_args['input_bucket']}] + aws_args['lambda']['input'] = [{'storage_provider' : 's3', 'path': s3_args['input_bucket']}] if 'output_bucket' in s3_args: - aws_args['lambda']['output'] = [{'storage_name' : s3_id, 'path': s3_args['output_bucket']}] - storage['storages'] = {'s3': [{'name': s3_id}]} + aws_args['lambda']['output'] = [{'storage_provider' : 's3', 'path': s3_args['output_bucket']}] + storage['storages'] = {'s3': []} return storage diff --git a/scar/providers/aws/__init__.py b/scar/providers/aws/__init__.py index 3c51d626..6425485d 100644 --- a/scar/providers/aws/__init__.py +++ b/scar/providers/aws/__init__.py @@ -39,14 +39,15 @@ class GenericClient(): 'S3': S3Client, 'LAUNCHTEMPLATES': EC2Client} - def __init__(self, client_properties: Dict): + def __init__(self, client_properties: Dict =None): self.properties = {} - region = client_properties.get('region') - if region: - self.properties['client'] = {'region_name': region} - session = client_properties.get('boto_profile') - if session: - self.properties['session'] = {'profile_name': session} + if client_properties: + region = client_properties.get('region') + if region: + self.properties['client'] = {'region_name': region} + session = client_properties.get('boto_profile') + if session: + self.properties['session'] = {'profile_name': session} @lazy_property def client(self): diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index c856d7ea..ea0f2704 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -25,7 +25,6 @@ from scar.providers.aws.resourcegroups import ResourceGroups from scar.providers.aws.s3 import S3 from scar.providers.aws.validators import AWSValidator -from scar.providers.aws.properties import ApiGatewayProperties import scar.exceptions as excp import scar.logger as logger import scar.providers.aws.response as response_parser @@ -54,30 +53,30 @@ def _get_owner(function): ############################################ -def _get_iam_client(aws_properties: Dict) -> IAM: - return IAM(aws_properties) +def _get_iam_client(function_info: Dict) -> IAM: + return IAM(function_info) -def _get_lambda_client(aws_properties: Dict=None) -> Lambda: - if not aws_properties: - aws_properties = {} - return Lambda(aws_properties) +def _get_lambda_client(function_info: Dict=None) -> Lambda: + if not function_info: + function_info = {} + return Lambda(function_info) -def _get_api_gateway_client(aws_properties: Dict) -> APIGateway: - return APIGateway(aws_properties) +def _get_api_gateway_client(function_info: Dict) -> APIGateway: + return APIGateway(function_info) -def _get_s3_client(aws_properties: Dict) -> S3: - return S3(aws_properties) +def _get_s3_client(function_info: Dict) -> S3: + return S3(function_info) -def _get_resource_groups_client(aws_properties: Dict) -> ResourceGroups: - return ResourceGroups(aws_properties) +def _get_resource_groups_client(function_info: Dict) -> ResourceGroups: + return ResourceGroups(function_info) -def _get_cloudwatch_logs_client(aws_properties: Dict) -> CloudWatchLogs: - return CloudWatchLogs(aws_properties) +def _get_cloudwatch_logs_client(function_info: Dict) -> CloudWatchLogs: + return CloudWatchLogs(function_info) ############################################ ### ADD EXTRA PROPERTIES ### @@ -108,14 +107,14 @@ def _add_handler(function: Dict): def _add_output(scar_props: Dict, function: Dict): - function['lambda']['output'] = response_parser.OutputType.PLAIN_TEXT.value + function['lambda']['cli_output'] = response_parser.OutputType.PLAIN_TEXT.value if scar_props.get("json", False): - function['lambda']['output'] = response_parser.OutputType.JSON.value + function['lambda']['cli_output'] = response_parser.OutputType.JSON.value # Override json ouput if both of them are defined if scar_props.get("verbose", False): - function['lambda']['output'] = response_parser.OutputType.VERBOSE.value + function['lambda']['cli_output'] = response_parser.OutputType.VERBOSE.value if scar_props.get("output_file", False): - function['lambda']['output'] = response_parser.OutputType.BINARY.value + function['lambda']['cli_output'] = response_parser.OutputType.BINARY.value function['lambda']['output_file'] = scar_props.get("output_file") @@ -178,7 +177,7 @@ def init(self) -> None: self._create_api_gateway(function) self._create_lambda_function(function, lambda_client) self._create_log_group(function) - # self._create_s3_buckets() + self._create_s3_buckets(function) # The api_gateway permissions are added after the function is created self._add_api_gateway_permissions(function) # self._create_batch_environment() @@ -222,7 +221,7 @@ def update(self): def ls(self): lambda_functions = self._get_all_functions() response_parser.parse_ls_response(lambda_functions, - self.aws_functions[0].get('lambda').get('output')) + self.aws_functions[0].get('lambda').get('cli_output')) 'TODO FINISH' # if self.storages: # file_list = _get_s3_client(self.aws_functions).get_bucket_file_list() @@ -302,16 +301,6 @@ def _get_batch_logs(self) -> str: logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - @excp.exception(logger) - def _create_s3_buckets(self): - if hasattr(self.aws_properties, "s3"): - if hasattr(self.aws_properties.s3, "input_bucket"): - self.aws_s3.create_input_bucket(create_input_folder=True) - self.aws_lambda.link_function_and_input_bucket() - self.aws_s3.set_input_bucket_notification() - if hasattr(self.aws_properties.s3, "output_bucket"): - self.aws_s3.create_output_bucket() - def _add_api_gateway_permissions(self, function: Dict): if function.get("api_gateway", {}).get('name', False): _get_lambda_client(function).add_invocation_permission_from_api_gateway() @@ -373,16 +362,16 @@ def _update_all_functions(self, lambda_functions): def _update_local_function_properties(self, function_info): self._reset_aws_properties() - """Update the defined properties with the AWS information.""" - if function_info: - self.aws_properties.lambdaf.update_properties(**function_info) - if 'API_GATEWAY_ID' in self.aws_properties.lambdaf.environment['Variables']: - api_gtw_id = self.aws_properties.lambdaf.environment['Variables'].get('API_GATEWAY_ID', - "") - if hasattr(self.aws_properties, 'api_gateway'): - self.aws_properties.api_gateway.id = api_gtw_id - else: - self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) +# """Update the defined properties with the AWS information.""" +# if function_info: +# self.aws_properties.lambdaf.update_properties(**function_info) +# if 'API_GATEWAY_ID' in self.aws_properties.lambdaf.environment['Variables']: +# api_gtw_id = self.aws_properties.lambdaf.environment['Variables'].get('API_GATEWAY_ID', +# "") +# if hasattr(self.aws_properties, 'api_gateway'): +# self.aws_properties.api_gateway.id = api_gtw_id +# else: +# self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) ############################################################################# ### Methods to create AWS resources ### @@ -405,7 +394,24 @@ def _create_log_group(self, function: Dict): response = cloudwatch_logs.create_log_group() response_parser.parse_log_group_creation_response(response, cloudwatch_logs.get_log_group_name(), - function.get('lambda').get('output')) + function.get('lambda').get('cli_output')) + + @excp.exception(logger) + def _create_s3_buckets(self, function: Dict): + if function.get('lambda').get('input', False): + s3 = _get_s3_client(function) + for bucket in function.get('lambda').get('input'): + if bucket.get('storage_provider') == 's3': + bucket_name = s3.create_bucket_and_folders(bucket.get('path')) + _get_lambda_client(function).link_function_and_bucket(bucket_name) + s3.set_input_bucket_notification(bucket_name) + + if function.get('lambda').get('output', False): + s3 = _get_s3_client(function) + for bucket in function.get('lambda').get('output'): + if bucket.get('storage_provider') == 's3': + s3.create_bucket_and_folders(bucket.get('path')) + ############################################################################# ### Methods to delete AWS resources ### ############################################################################# @@ -435,7 +441,7 @@ def _delete_api_gateway(self, function): response = _get_api_gateway_client(function).delete_api_gateway(api_gateway_id) response_parser.parse_delete_api_response(response, api_gateway_id, - function.get('lambda').get('output')) + function.get('lambda').get('cli_output')) def _delete_logs(self, function: Dict): cloudwatch_logs = _get_cloudwatch_logs_client(function) @@ -443,7 +449,7 @@ def _delete_logs(self, function: Dict): response = cloudwatch_logs.delete_log_group(log_group_name) response_parser.parse_delete_log_response(response, log_group_name, - function.get('lambda').get('output')) + function.get('lambda').get('cli_output')) def _delete_bucket_notifications(self, function_arn, function_env_vars): s3_provider_id = ""#_get_storage_provider_id('S3', function_env_vars) @@ -457,7 +463,7 @@ def _delete_lambda_function(self, function: Dict): response = _get_lambda_client(function).delete_function(function.get('lambda').get('name')) response_parser.parse_delete_function_response(response, function.get('lambda').get('name'), - function.get('lambda').get('output')) + function.get('lambda').get('cli_output')) def _delete_batch_resources(self, function_name): if self.batch.exist_compute_environments(function_name): diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index ac4ff71f..5703b36f 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -42,25 +42,19 @@ def _get_layers_client(client: LambdaClient, supervisor_version: str) -> LambdaL return LambdaLayers(client, supervisor_version) -def _get_s3_client(aws_properties: Dict) -> S3: - return S3(aws_properties) - - class Lambda(GenericClient): def __init__(self, aws_properties: Dict) -> None: super().__init__(aws_properties.get('lambda', {})) self.aws = aws_properties self.function = aws_properties.get('lambda', {}) - self.tmp_folder = FileUtils.create_tmp_dir() - self.zip_payload_path = FileUtils.join_paths(self.tmp_folder.name, 'function.zip') - def _get_creations_args(self): + def _get_creations_args(self, zip_payload_path: str) -> Dict: return {'FunctionName': self.function.get('name'), 'Runtime': self.function.get('runtime'), 'Role': self.aws.get('iam').get('role'), 'Handler': self.function.get('handler'), - 'Code': self._get_function_code(), + 'Code': self._get_function_code(zip_payload_path), 'Environment': self.function.get('environment'), 'Description': self.function.get('description'), 'Timeout': self.function.get('timeout'), @@ -77,8 +71,10 @@ def get_access_key(self) -> str: @excp.exception(logger) def create_function(self): + tmp_folder = FileUtils.create_tmp_dir() + zip_payload_path = FileUtils.join_paths(tmp_folder.name, 'function.zip') self._manage_supervisor_layer() - creation_args = self._get_creations_args() + creation_args = self._get_creations_args(zip_payload_path) response = self.client.create_function(**creation_args) if response and "FunctionArn" in response: self.function['arn'] = response.get('FunctionArn', "") @@ -90,27 +86,28 @@ def _manage_supervisor_layer(self): self.function.get('layers', []).append(layers_client.get_latest_supervisor_layer_arn()) @excp.exception(logger) - def _get_function_code(self): - # Zip all the files and folders needed + def _get_function_code(self, zip_payload_path: str) -> Dict: + '''Zip all the files and folders needed.''' code = {} - FunctionPackager(self.aws).create_zip(self.zip_payload_path) + FunctionPackager(self.aws).create_zip(zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" - self._get_s3_client().upload_file(file_path=self.zip_payload_path, - file_key=file_key) + S3(self.aws).upload_file(bucket=self.function.get('deployment').get('bucket'), + file_path=zip_payload_path, + file_key=file_key) code = {"S3Bucket": self.function.get('deployment_bucket'), "S3Key": file_key} else: - code = {"ZipFile": FileUtils.read_file(self.zip_payload_path, mode="rb")} + code = {"ZipFile": FileUtils.read_file(zip_payload_path, mode="rb")} return code def delete_function(self, function_name): return self.client.delete_function(function_name) - def link_function_and_input_bucket(self): - kwargs = {'FunctionName' : self.aws.lambdaf.name, + def link_function_and_bucket(self, bucket_name: str) -> None: + kwargs = {'FunctionName' : self.function.get('name'), 'Principal' : "s3.amazonaws.com", - 'SourceArn' : 'arn:aws:s3:::{0}'.format(self.aws.s3.input_bucket)} + 'SourceArn' : f'arn:aws:s3:::{bucket_name}'} self.client.add_invocation_permission(**kwargs) def preheat_function(self): @@ -262,7 +259,7 @@ def add_invocation_permission_from_api_gateway(self): api_id=api.get('id'))} self.client.add_invocation_permission(**kwargs) # Add Invocation permission - kwargs['SourceArn'] = api.get('source_arn_invocation').format(api_region=api.get('region'), + kwargs['SourceArn'] = api.get('source_arn_invocation').format(api_region=api.get('region'), account_id=self.aws.get('iam').get('account_id'), api_id=api.get('id'), stage_name=api.get('stage_name')) diff --git a/scar/providers/aws/properties.py b/scar/providers/aws/properties.py index f365411a..bac05a61 100644 --- a/scar/providers/aws/properties.py +++ b/scar/providers/aws/properties.py @@ -1,191 +1,191 @@ -# Copyright (C) GRyCAP - I3M - UPV -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module with classes and methods to manage the different -properties needed by SCAR and the boto clients.""" - - -class ScarProperties(dict): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - - -class AwsProperties(dict): - - def __init__(self, *args, **kwargs): - """ - {'account_id': '914332', - 'batch': see_batch_props_class, - 'boto_profile': 'default', - 'cloudwatch': see_cloudwatch_props_class, - 'config_path': 'cowsay', - 'execution_mode': 'lambda', - 'iam': see_iam_props_class, - 'lambda': see_lambdaf_props_class, - 'output': , - 'region': 'us-east-1', - 's3': see_s3_props_class, - 'tags': {'createdby': 'scar', 'owner': 'alpegon'}} - """ - super().__init__(*args, **kwargs) - self.__dict__ = self - self._initialize_properties() - - def _initialize_properties(self): - if hasattr(self, "api_gateway"): - self.api_gateway = ApiGatewayProperties(self.api_gateway) - if hasattr(self, "batch"): - self.batch = BatchProperties(self.batch) - if hasattr(self, "cloudwatch"): - self.cloudwatch = CloudWatchProperties(self.cloudwatch) - if hasattr(self, "iam"): - self.iam = IamProperties(self.iam) - if hasattr(self, "lambda"): - self.lambdaf = LambdaProperties(self.__dict__['lambda']) - self.__dict__.pop('lambda', None) - if hasattr(self, "s3"): - self.s3 = S3Properties(self.s3) - - -class ApiGatewayProperties(dict): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - - -class BatchProperties(dict): - """ - Example of dictionary used to initialize the class properties: - {'comp_type': 'EC2', - 'desired_v_cpus': 0, - 'instance_types': ['m3.medium'], - 'max_v_cpus': 2, - 'min_v_cpus': 0, - 'security_group_ids': ['sg-2568'], - 'state': 'ENABLED', - 'subnets': ['subnet-568', - 'subnet-569', - 'subnet-570', - 'subnet-571', - 'subnet-572'], - 'type': 'MANAGED'} - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - - -class LambdaProperties(dict): - """ - Example of dictionary used to initialize the class properties: - {'asynchronous': False, - 'description': 'Automatically generated lambda function', - 'environment': {'Variables': {'EXECUTION_MODE': 'lambda', - 'INPUT_BUCKET': 'test1', - 'LOG_LEVEL': 'INFO', - 'SUPERVISOR_TYPE': 'LAMBDA', - 'TIMEOUT_THRESHOLD': '10', - 'UDOCKER_BIN': '/opt/udocker/bin/', - 'UDOCKER_DIR': '/tmp/shared/udocker', - 'UDOCKER_EXEC': '/opt/udocker/udocker.py', - 'UDOCKER_LIB': '/opt/udocker/lib/'}}, - 'extra_payload': '/test/', - 'handler': 'test.lambda_handler', - 'image_file': 'minicow.tar.gz', - 'init_script': 'test.sh', - 'invocation_type': 'RequestResponse', - 'layers': ['arn:aws:lambda:us-east-1:914332:layer:faas-supervisor:1'], - 'log_level': 'INFO', - 'log_type': 'Tail', - 'memory': 512, - 'name': 'test', - 'runtime': 'python3.6', - 'time': 300, - 'timeout_threshold': 10, - 'zip_file_path': '/tmp/function.zip'} - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - - def update_properties(self, **kwargs): - if 'ResponseMetadata' in kwargs: - # Parsing RAW function info - self.description = kwargs['Description'] - self.environment = kwargs['Environment'] - self.arn = kwargs['FunctionArn'] - self.name = kwargs['FunctionName'] - self.handler = kwargs['Handler'] - self.layers = [layer['Arn'] for layer in kwargs['Layers']] - self.memory = kwargs['MemorySize'] - self.time = kwargs['Timeout'] - self.role = kwargs['Role'] - self.runtime = kwargs['Runtime'] - else: - self.__dict__.update(**kwargs) - - -class IamProperties(dict): - """ - Example of dictionary used to initialize the class properties: - {'role': 'arn:aws:iam::914332:role/invented-role'} - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - - -class S3Properties(dict): - """ - Example of dictionary used to initialize the class properties: - {'input_bucket': 'test1'} - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self - self.process_storage_paths() - - def process_storage_paths(self): - if hasattr(self, "input_bucket"): - self.storage_path_input = self.input_bucket - input_path = self.input_bucket.split("/", 1) - if len(input_path) > 1: - # There are folders defined - self.input_bucket = input_path[0] - self.input_folder = input_path[1] - - if hasattr(self, "output_bucket"): - self.storage_path_output = self.output_bucket - output_path = self.output_bucket.split("/", 1) - if len(output_path) > 1: - # There are folders defined - self.output_bucket = output_path[0] - self.output_folder = output_path[1] - - -class CloudWatchProperties(dict): - """ - Example of dictionary used to initialize the class properties: - {'log_retention_policy_in_days': 30} - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__dict__ = self +# # Copyright (C) GRyCAP - I3M - UPV +# # +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at +# # +# # http://www.apache.org/licenses/LICENSE-2.0 +# # +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. +# """Module with classes and methods to manage the different +# properties needed by SCAR and the boto clients.""" +# +# +# class ScarProperties(dict): +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# +# +# class AwsProperties(dict): +# +# def __init__(self, *args, **kwargs): +# """ +# {'account_id': '914332', +# 'batch': see_batch_props_class, +# 'boto_profile': 'default', +# 'cloudwatch': see_cloudwatch_props_class, +# 'config_path': 'cowsay', +# 'execution_mode': 'lambda', +# 'iam': see_iam_props_class, +# 'lambda': see_lambdaf_props_class, +# 'output': , +# 'region': 'us-east-1', +# 's3': see_s3_props_class, +# 'tags': {'createdby': 'scar', 'owner': 'alpegon'}} +# """ +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# self._initialize_properties() +# +# def _initialize_properties(self): +# if hasattr(self, "api_gateway"): +# self.api_gateway = ApiGatewayProperties(self.api_gateway) +# if hasattr(self, "batch"): +# self.batch = BatchProperties(self.batch) +# if hasattr(self, "cloudwatch"): +# self.cloudwatch = CloudWatchProperties(self.cloudwatch) +# if hasattr(self, "iam"): +# self.iam = IamProperties(self.iam) +# if hasattr(self, "lambda"): +# self.lambdaf = LambdaProperties(self.__dict__['lambda']) +# self.__dict__.pop('lambda', None) +# if hasattr(self, "s3"): +# self.s3 = S3Properties(self.s3) +# +# +# class ApiGatewayProperties(dict): +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# +# +# class BatchProperties(dict): +# """ +# Example of dictionary used to initialize the class properties: +# {'comp_type': 'EC2', +# 'desired_v_cpus': 0, +# 'instance_types': ['m3.medium'], +# 'max_v_cpus': 2, +# 'min_v_cpus': 0, +# 'security_group_ids': ['sg-2568'], +# 'state': 'ENABLED', +# 'subnets': ['subnet-568', +# 'subnet-569', +# 'subnet-570', +# 'subnet-571', +# 'subnet-572'], +# 'type': 'MANAGED'} +# """ +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# +# +# class LambdaProperties(dict): +# """ +# Example of dictionary used to initialize the class properties: +# {'asynchronous': False, +# 'description': 'Automatically generated lambda function', +# 'environment': {'Variables': {'EXECUTION_MODE': 'lambda', +# 'INPUT_BUCKET': 'test1', +# 'LOG_LEVEL': 'INFO', +# 'SUPERVISOR_TYPE': 'LAMBDA', +# 'TIMEOUT_THRESHOLD': '10', +# 'UDOCKER_BIN': '/opt/udocker/bin/', +# 'UDOCKER_DIR': '/tmp/shared/udocker', +# 'UDOCKER_EXEC': '/opt/udocker/udocker.py', +# 'UDOCKER_LIB': '/opt/udocker/lib/'}}, +# 'extra_payload': '/test/', +# 'handler': 'test.lambda_handler', +# 'image_file': 'minicow.tar.gz', +# 'init_script': 'test.sh', +# 'invocation_type': 'RequestResponse', +# 'layers': ['arn:aws:lambda:us-east-1:914332:layer:faas-supervisor:1'], +# 'log_level': 'INFO', +# 'log_type': 'Tail', +# 'memory': 512, +# 'name': 'test', +# 'runtime': 'python3.6', +# 'time': 300, +# 'timeout_threshold': 10, +# 'zip_file_path': '/tmp/function.zip'} +# """ +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# +# def update_properties(self, **kwargs): +# if 'ResponseMetadata' in kwargs: +# # Parsing RAW function info +# self.description = kwargs['Description'] +# self.environment = kwargs['Environment'] +# self.arn = kwargs['FunctionArn'] +# self.name = kwargs['FunctionName'] +# self.handler = kwargs['Handler'] +# self.layers = [layer['Arn'] for layer in kwargs['Layers']] +# self.memory = kwargs['MemorySize'] +# self.time = kwargs['Timeout'] +# self.role = kwargs['Role'] +# self.runtime = kwargs['Runtime'] +# else: +# self.__dict__.update(**kwargs) +# +# +# class IamProperties(dict): +# """ +# Example of dictionary used to initialize the class properties: +# {'role': 'arn:aws:iam::914332:role/invented-role'} +# """ +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# +# +# class S3Properties(dict): +# """ +# Example of dictionary used to initialize the class properties: +# {'input_bucket': 'test1'} +# """ +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self +# self.process_storage_paths() +# +# def process_storage_paths(self): +# if hasattr(self, "input_bucket"): +# self.storage_path_input = self.input_bucket +# input_path = self.input_bucket.split("/", 1) +# if len(input_path) > 1: +# # There are folders defined +# self.input_bucket = input_path[0] +# self.input_folder = input_path[1] +# +# if hasattr(self, "output_bucket"): +# self.storage_path_output = self.output_bucket +# output_path = self.output_bucket.split("/", 1) +# if len(output_path) > 1: +# # There are folders defined +# self.output_bucket = output_path[0] +# self.output_folder = output_path[1] +# +# +# class CloudWatchProperties(dict): +# """ +# Example of dictionary used to initialize the class properties: +# {'log_retention_policy_in_days': 30} +# """ +# +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.__dict__ = self diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index 16df355e..a0cb890a 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -72,7 +72,7 @@ def _print_generic_response(response, output_type, aws_output, text_message=None def parse_lambda_function_creation_response(response, function, access_key): if response: function_name = function.get('lambda', {}).get('name', '') - output_type = function.get('lambda', {}).get('output', '') + output_type = function.get('lambda', {}).get('cli_output', '') aws_output = 'LambdaOutput' text_message = f"Function '{function_name}' successfully created." json_message = {aws_output : { diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index e583b577..da368134 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -13,59 +13,56 @@ # limitations under the License. import os +from typing import Tuple, Dict from scar.providers.aws import GenericClient import scar.exceptions as excp import scar.logger as logger from scar.utils import FileUtils -from scar.providers.aws.properties import S3Properties + + +def _get_bucket_and_folders(storage_path: str) -> Tuple: + output_bucket = storage_path + output_folders = "" + output_path = storage_path.split("/", 1) + if len(output_path) > 1: + # There are folders defined + output_bucket = output_path[0] + output_folders = output_path[1] + return (output_bucket, output_folders) class S3(GenericClient): - def __init__(self, aws_properties): - super().__init__(aws_properties.get('lambda')) - - if hasattr(self._aws, 's3'): - if type(self._aws.s3) is dict: - self._aws.s3 = S3Properties(self._aws.s3) - self._initialize_properties() - - def _initialize_properties(self): - if not hasattr(self._aws.s3, "input_folder"): - self._aws.s3.input_folder = '' - if hasattr(self._aws.lambdaf, "name"): - self._aws.s3.input_folder = "{0}/input/".format(self._aws.lambdaf.name) - elif not self._aws.s3.input_folder.endswith("/"): - self._aws.s3.input_folder = "{0}/".format(self._aws.s3.input_folder) + def __init__(self, function_info): + super().__init__() + self.aws = function_info @excp.exception(logger) - def create_bucket(self, bucket_name): + def create_bucket(self, bucket_name) -> None: if not self.client.find_bucket(bucket_name): self.client.create_bucket(bucket_name) - def create_output_bucket(self): - self.create_bucket(self._aws.s3.output_bucket) - @excp.exception(logger) - def add_bucket_folder(self): - if self._aws.s3.input_folder: - self.upload_file(folder_name=self._aws.s3.input_folder) + def add_bucket_folder(self, folders: str) -> None: + self.upload_file(folder_name=folders) - def create_input_bucket(self, create_input_folder=False): - self.create_bucket(self._aws.s3.input_bucket) - if create_input_folder: - self.add_bucket_folder() + def create_bucket_and_folders(self, storage_path: str) -> str: + bucket, folders = _get_bucket_and_folders(storage_path) + self.create_bucket(bucket) + if folders: + self.add_bucket_folder(folders) + return bucket - def set_input_bucket_notification(self): + def set_input_bucket_notification(self, bucket_name: str) -> None: # First check that the function doesn't have other configurations - bucket_conf = self.client.get_notification_configuration(self._aws.s3.input_bucket) - trigger_conf = self.get_trigger_configuration() + bucket_conf = self.client.get_notification_configuration(bucket_name) + trigger_conf = self.get_trigger_configuration(bucket_name) lambda_conf = [trigger_conf] if "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] lambda_conf.append(trigger_conf) - notification = { "LambdaFunctionConfigurations": lambda_conf } - self.client.put_notification_configuration(self._aws.s3.input_bucket, notification) + notification = {"LambdaFunctionConfigurations": lambda_conf} + self.client.put_notification_configuration(bucket_name, notification) def delete_bucket_notification(self, bucket_name, function_arn): bucket_conf = self.client.get_notification_configuration(bucket_name) @@ -76,10 +73,10 @@ def delete_bucket_notification(self, bucket_name, function_arn): self.client.put_notification_configuration(bucket_name, notification) logger.info("Bucket notifications successfully deleted") - def get_trigger_configuration(self): - return {"LambdaFunctionArn": self._aws.lambdaf.arn, + def get_trigger_configuration(self, bucket_name: str) -> Dict: + return {"LambdaFunctionArn": self.aws.get('lambda').get('arn'), "Events": [ "s3:ObjectCreated:*" ], - "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": self._aws.s3.input_folder }]}} + "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": bucket_name }]}} } def get_file_key(self, folder_name=None, file_path=None, file_key=None): @@ -95,8 +92,8 @@ def get_file_key(self, folder_name=None, file_path=None, file_key=None): return file_key @excp.exception(logger) - def upload_file(self, folder_name=None, file_path=None, file_key=None): - kwargs = {'Bucket' : self._aws.s3.input_bucket} + def upload_file(self, bucket: str, folder_name: str =None, file_path: str =None, file_key: str =None) -> None: + kwargs = {'Bucket' : bucket} kwargs['Key'] = self.get_file_key(folder_name, file_path, file_key) if file_path: try: @@ -104,26 +101,26 @@ def upload_file(self, folder_name=None, file_path=None, file_key=None): except FileNotFoundError: raise excp.UploadFileNotFoundError(file_path=file_path) if folder_name and not file_path: - logger.info("Folder '{0}' created in bucket '{1}'".format(kwargs['Key'], kwargs['Bucket'])) + logger.info(f"Folder '{kwargs['Key']}' created in bucket '{kwargs['Bucket']}'") else: - logger.info("Uploading file '{0}' to bucket '{1}' with key '{2}'".format(file_path, kwargs['Bucket'], kwargs['Key'])) + logger.info(f"Uploading file '{file_path}' to bucket '{kwargs['Bucket']}' with key '{kwargs['Key']}'") self.client.upload_file(**kwargs) @excp.exception(logger) def get_bucket_file_list(self): - bucket_name = self._aws.s3.input_bucket + bucket_name = self.aws.s3.input_bucket if self.client.find_bucket(bucket_name): kwargs = {"Bucket" : bucket_name} - if hasattr(self._aws.s3, "input_folder") and self._aws.s3.input_folder: - kwargs["Prefix"] = self._aws.s3.input_folder + if hasattr(self.aws.s3, "input_folder") and self.aws.s3.input_folder: + kwargs["Prefix"] = self.aws.s3.input_folder return self.client.list_files(**kwargs) else: raise excp.BucketNotFoundError(bucket_name=bucket_name) def get_s3_event(self, s3_file_key): - return {"Records": [{"eventSource": "_aws:s3", - "s3" : {"bucket" : {"name": self._aws.s3.input_bucket, - "arn": f'arn:_aws:s3:::{self._aws.s3.input_bucket}'}, + return {"Records": [{"eventSource": "aws:s3", + "s3" : {"bucket" : {"name": self.aws.s3.input_bucket, + "arn": f'arn:aws:s3:::{self.aws.s3.input_bucket}'}, "object" : {"key": s3_file_key}}}]} def get_s3_event_list(self, s3_file_keys): From 84bc43265acbbd3947b6dd76c740475af996c532 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Thu, 21 Nov 2019 20:56:37 +0100 Subject: [PATCH 09/41] WIP - Add batch resources --- scar/parser/cfgfile.py | 20 +- scar/providers/aws/__init__.py | 11 +- scar/providers/aws/apigateway.py | 20 +- scar/providers/aws/batchfunction.py | 192 +++++++---------- scar/providers/aws/cloudwatchlogs.py | 16 +- scar/providers/aws/controller.py | 297 ++++++++++++-------------- scar/providers/aws/functioncode.py | 47 ++-- scar/providers/aws/iam.py | 4 +- scar/providers/aws/lambdafunction.py | 154 ++++++------- scar/providers/aws/lambdalayers.py | 28 +-- scar/providers/aws/launchtemplates.py | 21 +- scar/providers/aws/properties.py | 191 ----------------- scar/providers/aws/resourcegroups.py | 4 +- scar/providers/aws/s3.py | 20 +- scar/providers/aws/udocker.py | 24 +-- 15 files changed, 391 insertions(+), 658 deletions(-) delete mode 100644 scar/providers/aws/properties.py diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index 4042e5de..ef85256d 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -21,7 +21,7 @@ _DEFAULT_CFG = { "scar": { - "config_version": "1.0.6" + "config_version": "1.0.8" }, "aws": { "iam": {"boto_profile": "default", @@ -50,7 +50,8 @@ "max_s3_payload_size": 262144000 }, "container": { - "environment_variables": {}, + "environment" : { + "Variables" : {}}, "timeout_threshold": 10 }, # Must be a Github tag or "latest" @@ -99,17 +100,22 @@ "vcpus": 1, "memory": 1024, "enable_gpu": False, + "state": "ENABLED", + "type": "MANAGED", + "environment" : { + "Variables" : {}}, "compute_resources": { - "state": "ENABLED", - "type": "MANAGED", "security_group_ids": [], - "comp_type": "EC2", + "type": "EC2", "desired_v_cpus": 0, "min_v_cpus": 0, "max_v_cpus": 2, "subnets": [], - "instance_types": ["m3.medium"] - } + "instance_types": ["m3.medium"], + "launch_template_name": "faas-supervisor", + "instance_role": "arn:aws:iam::{account_id}:instance-profile/ecsInstanceRole" + }, + "service_role": "arn:aws:iam::{account_id}:role/service-role/AWSBatchServiceRole" } } } diff --git a/scar/providers/aws/__init__.py b/scar/providers/aws/__init__.py index 6425485d..195c17af 100644 --- a/scar/providers/aws/__init__.py +++ b/scar/providers/aws/__init__.py @@ -39,19 +39,18 @@ class GenericClient(): 'S3': S3Client, 'LAUNCHTEMPLATES': EC2Client} - def __init__(self, client_properties: Dict =None): + def __init__(self, resource_info: Dict =None): self.properties = {} - if client_properties: - region = client_properties.get('region') + if resource_info: + region = resource_info.get('region') if region: self.properties['client'] = {'region_name': region} - session = client_properties.get('boto_profile') + session = resource_info.get('boto_profile') if session: self.properties['session'] = {'profile_name': session} @lazy_property def client(self): """Returns the required boto client based on the implementing class name.""" - client_name = self.__class__.__name__.upper() - client = self._CLIENTS[client_name](self.properties) + client = self._CLIENTS[self.__class__.__name__.upper()](self.properties) return client diff --git a/scar/providers/aws/apigateway.py b/scar/providers/aws/apigateway.py index 25971df9..b9f25d26 100644 --- a/scar/providers/aws/apigateway.py +++ b/scar/providers/aws/apigateway.py @@ -21,10 +21,10 @@ class APIGateway(GenericClient): """Manage the calls to the ApiGateway client.""" - def __init__(self, aws_properties: Dict): - super().__init__(aws_properties.get('api_gateway', {})) - self.aws = aws_properties - self.api = self.aws.get('api_gateway', {}) + def __init__(self, resources_info: Dict): + super().__init__(resources_info.get('api_gateway', {})) + self.resources_info = resources_info + self.api = self.resources_info.get('api_gateway', {}) def _get_common_args(self) -> Dict: return {'restApiId' : self.api.get('id', ''), @@ -39,9 +39,9 @@ def _get_method_args(self) -> Dict: def _get_integration_args(self) -> Dict: integration_args = self.api.get('integration', {}) uri_args = {'api_region': self.api.get('region', ''), - 'lambda_region': self.aws.get('lambda', {}).get('region', ''), - 'account_id': self.aws.get('iam', {}).get('account_id', ''), - 'function_name': self.aws.get('lambda', {}).get('name', '')} + 'lambda_region': self.resources_info.get('lambda', {}).get('region', ''), + 'account_id': self.resources_info.get('iam', {}).get('account_id', ''), + 'function_name': self.resources_info.get('lambda', {}).get('name', '')} integration_args['uri'] = integration_args['uri'].format(**uri_args) args = self._get_common_args() args.update(integration_args) @@ -60,7 +60,7 @@ def _set_api_gateway_id(self, api_info: Dict) -> None: self.api['id'] = api_info.get('id', '') # We store the parameter in the lambda configuration that # is going to be uploaded to the Lambda service - self.aws['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_info.get('id', '') + self.resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_info.get('id', '') def _set_resource_info_id(self, resource_info: Dict) -> None: self.api['resource_id'] = resource_info.get('id', '') @@ -82,6 +82,6 @@ def create_api_gateway(self) -> None: self.client.create_deployment(self.api.get('id', ''), self.api.get('stage_name', '')) logger.info(f'API Gateway endpoint: {self._get_endpoint()}') - def delete_api_gateway(self, api_gateway_id: str) -> None: + def delete_api_gateway(self) -> None: """Deletes an Api Gateway endpoint.""" - return self.client.delete_rest_api(api_gateway_id) + return self.client.delete_rest_api(self.resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID']) diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index 47c2b7c6..d6b671d2 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -12,14 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import random from typing import Dict, List from scar.providers.aws import GenericClient import scar.logger as logger from scar.providers.aws.launchtemplates import LaunchTemplates -from scar.utils import lazy_property, FileUtils, StrUtils - -_LAUNCH_TEMPLATE_NAME = 'faas-supervisor' +from scar.utils import FileUtils, StrUtils def _get_job_definitions(jobs_info: Dict) -> List: @@ -29,170 +26,127 @@ def _get_job_definitions(jobs_info: Dict) -> List: class Batch(GenericClient): - @lazy_property - def launch_templates(self): - launch_templates = LaunchTemplates(self._aws, self.supervisor_version) - return launch_templates - - def __init__(self, aws_properties, supervisor_version): - super().__init__(aws_properties) - self.supervisor_version = supervisor_version - self._aws.batch.instance_role = (f"arn:_aws:iam::{self._aws.account_id}:" - "instance-profile/ecsInstanceRole") - self._aws.batch.service_role = (f"arn:_aws:iam::{self._aws.account_id}:" - "role/service-role/AWSBatchServiceRole") - self._aws.batch.env_vars = [] + def __init__(self, resources_info): + super().__init__(resources_info.get('batch')) + self.resources_info = resources_info + self.batch = resources_info.get('batch') + self.function_name = self.resources_info.get('lambda').get('name') - def _set_required_environment_variables(self): - self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self._aws.lambdaf.name) + def _set_required_environment_variables(self) -> None: + self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self.function_name) self._set_batch_environment_variable('SCRIPT', self._get_user_script()) - if (hasattr(self._aws.lambdaf, 'environment_variables') and - self._aws.lambdaf.environment_variables): - self._add_custom_environment_variables(self._aws.lambdaf.environment_variables) - if (hasattr(self._aws.lambdaf, 'lambda_environment') and - self._aws.lambdaf.lambda_environment): - self._add_custom_environment_variables(self._aws.lambdaf.lambda_environment) - if hasattr(self._aws, "s3"): - self._add_s3_environment_vars() - - def _add_custom_environment_variables(self, env_vars): - if isinstance(env_vars, dict): - for key, val in env_vars.items(): - self._set_batch_environment_variable(key, val) - else: - for env_var in env_vars: - self._set_batch_environment_variable(*env_var.split("=")) - - def _set_batch_environment_variable(self, key, value): - if key and value is not None: - self._aws.batch.env_vars.append({'name': key, 'value': value}) + if self.resources_info.get('lambda').get('container').get('environment_variables', False): + for key, value in self.resources_info.get('lambda').get('container').get('environment_variables').items(): + self._set_batch_environment_variable(key, value) - def _add_s3_environment_vars(self): - provider_id = random.randint(1, 1000001) - if hasattr(self._aws.s3, "input_bucket"): - self._set_batch_environment_variable(f'STORAGE_PATH_INPUT_{provider_id}', - self._aws.s3.storage_path_input) - if hasattr(self._aws.s3, "output_bucket"): - self._set_batch_environment_variable(f'STORAGE_PATH_OUTPUT_{provider_id}', - self._aws.s3.storage_path_output) - else: - self._set_batch_environment_variable(f'STORAGE_PATH_OUTPUT_{provider_id}', - self._aws.s3.storage_path_input) - self._set_batch_environment_variable(f'STORAGE_AUTH_S3_USER_{provider_id}', 'scar') + def _set_batch_environment_variable(self, key: str, value: str) -> None: + self.resources_info['batch']['environment']['Variables'].update({'name': key, 'value': value}) - def _get_user_script(self): + def _get_user_script(self) -> str: script = '' - if hasattr(self._aws.lambdaf, "init_script"): - file_content = FileUtils.read_file(self._aws.lambdaf.init_script) + if self.resources_info.get('lambda').get('init_script', False): + file_content = FileUtils.read_file(self.resources_info.get('lambda').get('init_script')) script = StrUtils.utf8_to_base64_string(file_content) return script - def _delete_job_definitions(self, name): - job_definitions = [] - # Get IO definitions (if any) - kwargs = {"jobDefinitionName": '{0}-io'.format(name)} - io_job_info = self.client.describe_job_definitions(**kwargs) - job_definitions.extend(_get_job_definitions(io_job_info)) + def _delete_job_definitions(self) -> None: # Get main job definition - kwargs = {"jobDefinitionName": name} + kwargs = {"jobDefinitionName": self.function_name} job_info = self.client.describe_job_definitions(**kwargs) - job_definitions.extend(_get_job_definitions(job_info)) - for job_def in job_definitions: + for job_def in _get_job_definitions(job_info): kwars = {"jobDefinition": job_def} self.client.deregister_job_definition(**kwars) logger.info("Job definitions deleted") - def _get_job_queue_info(self, name): - job_queue_info_args = {'jobQueues': [self._get_resource_name(name)]} + def _get_job_queue_info(self): + job_queue_info_args = {'jobQueues': [self.function_name]} return self.client.describe_job_queues(**job_queue_info_args) - def _delete_job_queue(self, name): - response = self._get_job_queue_info(name) + def _delete_job_queue(self): + response = self._get_job_queue_info() while response["jobQueues"]: state = response["jobQueues"][0]["state"] status = response["jobQueues"][0]["status"] if status == "VALID": - self._delete_valid_job_queue(state, name) - response = self._get_job_queue_info(name) + self._delete_valid_job_queue(state) + response = self._get_job_queue_info() - def _delete_valid_job_queue(self, state, name): + def _delete_valid_job_queue(self, state): if state == "ENABLED": - updating_args = {'jobQueue': self._get_resource_name(name), + updating_args = {'jobQueue': self.function_name, 'state': 'DISABLED'} self.client.update_job_queue(**updating_args) elif state == "DISABLED": - deleting_args = {'jobQueue': self._get_resource_name(name)} + deleting_args = {'jobQueue': self.function_name} logger.info("Job queue deleted") self.client.delete_job_queue(**deleting_args) - def _get_compute_env_info(self, name): - creation_args = self._get_describe_compute_env_args(name_c=name) + def _get_describe_compute_env_args(self): + return {'computeEnvironments': [self.function_name]} + + def _get_compute_env_info(self): + creation_args = self._get_describe_compute_env_args() return self.client.describe_compute_environments(**creation_args) - def _delete_compute_env(self, name): - response = self._get_compute_env_info(name) + def _delete_compute_env(self): + response = self._get_compute_env_info() while response["computeEnvironments"]: state = response["computeEnvironments"][0]["state"] status = response["computeEnvironments"][0]["status"] if status == "VALID": - self._delete_valid_compute_environment(state, name) - response = self._get_compute_env_info(name) + self._delete_valid_compute_environment(state) + response = self._get_compute_env_info() - def _delete_valid_compute_environment(self, state, name): + def _delete_valid_compute_environment(self, state): if state == "ENABLED": - update_args = {'computeEnvironment': self._get_resource_name(name), + update_args = {'computeEnvironment': self.function_name, 'state': 'DISABLED'} self.client.update_compute_environment(**update_args) elif state == "DISABLED": - delete_args = {'computeEnvironment': self._get_resource_name(name)} + delete_args = {'computeEnvironment': self.function_name} logger.info("Compute environment deleted") self.client.delete_compute_environment(**delete_args) def _get_compute_env_args(self): + account_id = self.resources_info.get('iam').get('account_id') return { - 'computeEnvironmentName': self._aws.lambdaf.name, - 'serviceRole': self._aws.batch.service_role, - 'type': self._aws.batch.compute_resources['type'], - 'state': self._aws.batch.compute_resources['state'], + 'computeEnvironmentName': self.function_name, + 'serviceRole': self.batch.get('service_role').format(account_id=account_id), + 'type': self.batch.get('type'), + 'state': self.batch.get('state'), 'computeResources': { - 'type': self._aws.batch.compute_resources['comp_type'], - 'minvCpus': self._aws.batch.compute_resources['min_v_cpus'], - 'maxvCpus': self._aws.batch.compute_resources['max_v_cpus'], - 'desiredvCpus': self._aws.batch.compute_resources['desired_v_cpus'], - 'instanceTypes': self._aws.batch.compute_resources['instance_types'], - 'subnets': self._aws.batch.compute_resources['subnets'], - 'securityGroupIds': self._aws.batch.compute_resources['security_group_ids'], - 'instanceRole': self._aws.batch.instance_role, + 'type': self.batch.get('compute_resources').get('type'), + 'minvCpus': self.batch.get('compute_resources').get('min_v_cpus'), + 'maxvCpus': self.batch.get('compute_resources').get('max_v_cpus'), + 'desiredvCpus': self.batch.get('compute_resources').get('desired_v_cpus'), + 'instanceTypes': self.batch.get('compute_resources').get('instance_types'), + 'subnets': self.batch.get('compute_resources').get('subnets'), + 'securityGroupIds': self.batch.get('compute_resources').get('security_group_ids'), + 'instanceRole': self.batch.get('compute_resources').get('instance_role').format(account_id=account_id), 'launchTemplate': { - 'launchTemplateName': _LAUNCH_TEMPLATE_NAME, - 'version': str(self.launch_templates.get_launch_template_version()) + 'launchTemplateName': self.batch.get('compute_resources').get('launch_template_name'), + 'version': str(LaunchTemplates(self.resources_info).get_launch_template_version()) } } } def _get_creations_job_queue_args(self): return { - 'computeEnvironmentOrder': [{'computeEnvironment': self._aws.lambdaf.name, + 'computeEnvironmentOrder': [{'computeEnvironment': self.function_name, 'order': 1}, ], - 'jobQueueName': self._aws.lambdaf.name, + 'jobQueueName': self.function_name, 'priority': 1, - 'state': self._aws.batch.compute_resources['state'], + 'state': self.batch.get('compute_resources').get('state'), } - def _get_resource_name(self, name=None): - return name if name else self._aws.lambdaf.name - - def _get_describe_compute_env_args(self, name_c=None): - return {'computeEnvironments': [self._get_resource_name(name_c)]} - def _get_job_definition_args(self): job_def_args = { - 'jobDefinitionName': self._aws.lambdaf.name, + 'jobDefinitionName': self.function_name, 'type': 'container', 'containerProperties': { - 'image': self._aws.lambdaf.image, - 'memory': int(self._aws.batch.memory), - 'vcpus': int(self._aws.batch.vcpus), + 'image': self.resources_info.get('lambda').get('image'), + 'memory': int(self.batch.get('memory')), + 'vcpus': int(self.batch.get('vcpus')), 'command': [ '/bin/sh', '-c', @@ -206,7 +160,7 @@ def _get_job_definition_args(self): 'name': 'supervisor-bin' } ], - 'environment': self._aws.batch.env_vars, + 'environment': [{key, value} for key, value in self.resources_info['batch']['environment']['Variables'].items()], 'mountPoints': [ { 'containerPath': '/opt/faas-supervisor/bin', @@ -215,7 +169,7 @@ def _get_job_definition_args(self): ] } } - if self._aws.batch.enable_gpu: + if self.batch.get('enable_gpu'): job_def_args['containerProperties']['resourceRequirements'] = [ { 'value': '1', @@ -224,8 +178,8 @@ def _get_job_definition_args(self): ] return job_def_args - def _get_state_and_status_of_compute_env(self, name=None): - creation_args = self._get_describe_compute_env_args(name_c=name) + def _get_state_and_status_of_compute_env(self): + creation_args = self._get_describe_compute_env_args() response = self.client.describe_compute_environments(**creation_args) return (response["computeEnvironments"][0]["state"], response["computeEnvironments"][0]["status"]) @@ -242,16 +196,16 @@ def create_batch_environment(self): self.client.create_job_queue(**creation_args) logger.info('Job queue successfully created.') creation_args = self._get_job_definition_args() - logger.info(f"Registering '{self._aws.lambdaf.name}' job definition.") + logger.info(f"Registering '{self.function_name}' job definition.") return self.client.register_job_definition(**creation_args) - def delete_compute_environment(self, name): - self._delete_job_definitions(name) - self._delete_job_queue(name) - self._delete_compute_env(name) + def delete_compute_environment(self): + self._delete_job_definitions() + self._delete_job_queue() + self._delete_compute_env() - def exist_compute_environments(self, name): - creation_args = self._get_describe_compute_env_args(name_c=name) + def exist_compute_environments(self): + creation_args = self._get_describe_compute_env_args() response = self.client.describe_compute_environments(**creation_args) return len(response["computeEnvironments"]) > 0 diff --git a/scar/providers/aws/cloudwatchlogs.py b/scar/providers/aws/cloudwatchlogs.py index 0da148e6..8e08c398 100644 --- a/scar/providers/aws/cloudwatchlogs.py +++ b/scar/providers/aws/cloudwatchlogs.py @@ -29,17 +29,17 @@ def _parse_events_in_message(log_events: List) -> str: class CloudWatchLogs(GenericClient): """Manages the AWS CloudWatch Logs functionality""" - def __init__(self, aws_properties: Dict): - super().__init__(aws_properties.get('cloudwatch')) - self._aws = aws_properties - self.cloudwatch = aws_properties.get('cloudwatch') + def __init__(self, resources_info: Dict): + super().__init__(resources_info.get('cloudwatch')) + self.resources_info = resources_info + self.cloudwatch = resources_info.get('cloudwatch') def get_log_group_name(self, function_name: str=None) -> str: """Returns the log group matching the current lambda function being parsed.""" if function_name: return f'/aws/lambda/{function_name}' - return f'/aws/lambda/{self._aws.get("lambda").get("name")}' + return f'/aws/lambda/{self.resources_info.get("lambda").get("name")}' def _get_log_group_name_arg(self, function_name: str=None) -> Dict: return {'logGroupName' : self.get_log_group_name(function_name)} @@ -68,7 +68,7 @@ def _parse_logs_with_requestid(self, function_logs: str) -> str: def create_log_group(self) -> Dict: """Creates a CloudWatch Log Group.""" creation_args = self._get_log_group_name_arg() - creation_args['tags'] = self._aws.get('lambda').get('tags') + creation_args['tags'] = self.resources_info.get('lambda').get('tags') response = self.client.create_log_group(**creation_args) # Set retention policy into the log group retention_args = self._get_log_group_name_arg() @@ -80,7 +80,7 @@ def delete_log_group(self, log_group_name: str) -> Dict: """Deletes a CloudWatch Log Group.""" return self.client.delete_log_group(log_group_name) - def get_aws_log(self) -> str: + def getaws_log(self) -> str: """Returns Lambda logs for an specific lambda function.""" function_logs = "" try: @@ -100,7 +100,7 @@ def get_batch_job_log(self, jobs_info: List) -> str: if jobs_info: job = jobs_info[0] batch_logs += f"Batch job status: {job.get('status', '')}\n" - kwargs = {'logGroupName': "/_aws/batch/job"} + kwargs = {'logGroupName': "/aws/batch/job"} if job.get("status", "") == "SUCCEEDED": kwargs['logStreamNames'] = [job.get("container", {}).get("logStreamName", "")] batch_events = self.client.get_log_events(**kwargs) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index ea0f2704..46cf32ad 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -28,11 +28,10 @@ import scar.exceptions as excp import scar.logger as logger import scar.providers.aws.response as response_parser -from scar.utils import lazy_property, StrUtils, FileUtils +from scar.utils import StrUtils, FileUtils _ACCOUNT_ID_REGEX = r'\d{12}' - # def _get_storage_provider_id(storage_provider: str, env_vars: Dict) -> str: # """Searches the storage provider id in the environment variables: # get_provider_id(S3, {'STORAGE_AUTH_S3_USER_41807' : 'scar'}) @@ -45,38 +44,16 @@ # return res -def _get_owner(function): - return _get_iam_client(function).get_user_name_or_id() - -############################################ -### BOTO CLIENTS ### -############################################ - - -def _get_iam_client(function_info: Dict) -> IAM: - return IAM(function_info) - - -def _get_lambda_client(function_info: Dict=None) -> Lambda: - if not function_info: - function_info = {} - return Lambda(function_info) - - -def _get_api_gateway_client(function_info: Dict) -> APIGateway: - return APIGateway(function_info) - +def _get_owner(resources_info: Dict): + return IAM(resources_info).get_user_name_or_id() -def _get_s3_client(function_info: Dict) -> S3: - return S3(function_info) +def _check_function_defined(resources_info: Dict): + if Lambda(resources_info).find_function(): + raise excp.FunctionExistsError(function_name=resources_info.get('lambda', {}).get('name', '')) - -def _get_resource_groups_client(function_info: Dict) -> ResourceGroups: - return ResourceGroups(function_info) - - -def _get_cloudwatch_logs_client(function_info: Dict) -> CloudWatchLogs: - return CloudWatchLogs(function_info) +def _check_function_not_defined(resources_info: Dict): + if not Lambda(resources_info).find_function(): + raise excp.FunctionNotFoundError(function_name=resources_info.get('lambda', {}).get('name', '')) ############################################ ### ADD EXTRA PROPERTIES ### @@ -132,6 +109,115 @@ def _add_config_file_path(scar_props: Dict, function: Dict): function['lambda']['run_script'] = FileUtils.join_paths(function['lambda']['config_path'], function['lambda']['run_script']) +############################################################################# +### Methods to create AWS resources ### +############################################################################# + +@excp.exception(logger) +def _create_api_gateway(resources_info: Dict): + if resources_info.get("api_gateway", {}).get('name', False): + APIGateway(resources_info).create_api_gateway() + +@excp.exception(logger) +def _create_lambda_function(resources_info: Dict) -> None: + lambda_client = Lambda(resources_info) + response = lambda_client.create_function() + response_parser.parse_lambda_function_creation_response(response, + resources_info, + lambda_client.get_access_key()) + +@excp.exception(logger) +def _create_log_group(resources_info: Dict) -> None: + cloudwatch_logs = CloudWatchLogs(resources_info) + response = cloudwatch_logs.create_log_group() + response_parser.parse_log_group_creation_response(response, + cloudwatch_logs.get_log_group_name(), + resources_info.get('lambda').get('cli_output')) + +@excp.exception(logger) +def _create_s3_buckets(resources_info: Dict) -> None: + if resources_info.get('lambda').get('input', False): + s3 = S3(resources_info) + for bucket in resources_info.get('lambda').get('input'): + if bucket.get('storage_provider') == 's3': + bucket_name = s3.create_bucket_and_folders(bucket.get('path')) + Lambda(resources_info).link_function_and_bucket(bucket_name) + s3.set_input_bucket_notification(bucket_name) + + if resources_info.get('lambda').get('output', False): + s3 = Lambda(resources_info) + for bucket in resources_info.get('lambda').get('output'): + if bucket.get('storage_provider') == 's3': + s3.create_bucket_and_folders(bucket.get('path')) + +@excp.exception(logger) +def _add_api_gateway_permissions(resources_info: Dict): + if resources_info.get("api_gateway").get('name', False): + Lambda(resources_info).add_invocation_permission_from_api_gateway() + +@excp.exception(logger) +def _create_batch_environment(resources_info: Dict) -> None: + mode = resources_info.get('lambda').get('execution_mode') + if mode == "batch" or mode == "lambda-batch": + Batch(resources_info).create_batch_environment() + +############################################################################# +### Methods to delete AWS resources ### +############################################################################# + +def _delete_all_resources(): + 'TODO' +# for function_info in self._get_all_functions(): +# self._delete_resources(function_info) + +def _delete_resources(resources_info: Dict) -> None: + # Delete associated api + _delete_api_gateway(resources_info) + # Delete associated log + _delete_logs(resources_info) + # Delete associated notifications + _delete_bucket_notifications(resources_info) + # Delete function + _delete_lambda_function(resources_info) + # Delete resources batch + _delete_batch_resources(resources_info) + +def _delete_api_gateway(resources_info: Dict) -> None: + api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') + if api_gateway_id: + resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id + response = APIGateway(resources_info).delete_api_gateway() + response_parser.parse_delete_api_response(response, + api_gateway_id, + resources_info.get('lambda').get('cli_output')) + +def _delete_logs(resources_info: Dict): + cloudwatch_logs = CloudWatchLogs(resources_info) + log_group_name = cloudwatch_logs.get_log_group_name(resources_info.get('lambda').get('name')) + response = cloudwatch_logs.delete_log_group(log_group_name) + response_parser.parse_delete_log_response(response, + log_group_name, + resources_info.get('lambda').get('cli_output')) + +def _delete_bucket_notifications(resources_info: Dict) -> None: + if resources_info.get('lambda').get('input', False): + for input_storage in resources_info.get('lambda').get('input'): + if input_storage.get('storage_provider') == 's3': + bucket_name = input_storage.get('path').split("/", 1)[0] + S3(resources_info).delete_bucket_notification(bucket_name) + +def _delete_lambda_function(resources_info: Dict) -> None: + response = Lambda(resources_info).delete_function() + response_parser.parse_delete_function_response(response, + resources_info.get('lambda').get('name'), + resources_info.get('lambda').get('cli_output')) + +def _delete_batch_resources(resources_info: Dict) -> None: + batch = Batch(resources_info) + if batch.exist_compute_environments(): + batch.delete_compute_environment() + + ############################################ ### AWS CONTROLLER ### ############################################ @@ -141,12 +227,6 @@ class AWS(Commands): """AWS controller. Used to manage all the AWS calls and functionalities.""" - @lazy_property - def batch(self): - batch = Batch(self.aws_properties, - self.scar.supervisor_version) - return batch - def __init__(self, func_call): self.raw_args = FileUtils.load_config_file() self.validate_arguments(self.raw_args) @@ -169,18 +249,16 @@ def validate_arguments(self, merged_args: Dict) -> None: @excp.exception(logger) def init(self) -> None: # supervisor_version = self.scar.get('supervisor_version', 'latest') - for function in self.aws_functions: - lambda_client = _get_lambda_client(function) - if lambda_client.find_function(): - raise excp.FunctionExistsError(function_name=function.get('lambda', {}).get('name', '')) + for resources_info in self.aws_functions: + _check_function_defined(resources_info) # We have to create the gateway before creating the function - self._create_api_gateway(function) - self._create_lambda_function(function, lambda_client) - self._create_log_group(function) - self._create_s3_buckets(function) + _create_api_gateway(resources_info) + _create_lambda_function(resources_info) + _create_log_group(resources_info) + _create_s3_buckets(resources_info) # The api_gateway permissions are added after the function is created - self._add_api_gateway_permissions(function) - # self._create_batch_environment() + _add_api_gateway_permissions(resources_info) + _create_batch_environment() # self._preheat_function() @excp.exception(logger) @@ -196,8 +274,8 @@ def invoke(self): @excp.exception(logger) def run(self): - function = self.aws_functions[0] - lambda_client = _get_lambda_client(function) + resources_info = self.aws_functions[0] + lambda_client = Lambda(resources_info) if lambda_client.is_asynchronous(): lambda_client.set_asynchronous_call_parameters() lambda_client.launch_lambda_instance() @@ -243,12 +321,9 @@ def rm(self): "Please select the function to delete" else: "Delete selected function" - function = self.aws_functions[0] - lambda_client = _get_lambda_client(function) -# function_info = lambda_client.get_function_info(function.get('lambda').get('name')) - if not lambda_client.find_function(function.get('lambda').get('name')): - raise excp.FunctionNotFoundError(function_name=function.get('lambda').get('name')) - self._delete_resources(function) + resources_info = self.aws_functions[0] + _check_function_not_defined(resources_info) + self._delete_resources(resources_info) # function = self.aws_functions[0] # lambda_client = _get_lambda_client(function) @@ -289,9 +364,9 @@ def _get_all_functions(self): # iam_info = _get_iam_client(function).get_user_name_or_id() # functions_arn.union(set(_get_resource_groups_client(function).get_resource_arn_list(iam_info))) # return _get_lambda_client(self.aws_functions[0]).get_all_functions(functions_arn) - function = self.aws_functions[0] - arn_list = _get_resource_groups_client(function).get_resource_arn_list(_get_iam_client(function).get_user_name_or_id()) - return _get_lambda_client(function).get_all_functions(arn_list) + resources_info = self.aws_functions[0] + arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id()) + return Lambda(resources_info).get_all_functions(arn_list) def _get_batch_logs(self) -> str: logs = "" @@ -301,15 +376,6 @@ def _get_batch_logs(self) -> str: logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - def _add_api_gateway_permissions(self, function: Dict): - if function.get("api_gateway", {}).get('name', False): - _get_lambda_client(function).add_invocation_permission_from_api_gateway() - - def _create_batch_environment(self): - if self.aws_properties.execution_mode == "batch" or \ - self.aws_properties.execution_mode == "lambda-batch": - self.batch.create_batch_environment() - def _preheat_function(self): # If preheat is activated, the function is launched at the init step if hasattr(self.scar, "preheat"): @@ -373,98 +439,3 @@ def _update_local_function_properties(self, function_info): # else: # self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) -############################################################################# -### Methods to create AWS resources ### -############################################################################# - - def _create_api_gateway(self, function: Dict): - if function.get("api_gateway", {}).get('name', False): - _get_api_gateway_client(function).create_api_gateway() - - @excp.exception(logger) - def _create_lambda_function(self, function: Dict, lambda_client: Lambda) -> None: - response = lambda_client.create_function() - response_parser.parse_lambda_function_creation_response(response, - function, - lambda_client.get_access_key()) - - @excp.exception(logger) - def _create_log_group(self, function: Dict): - cloudwatch_logs = _get_cloudwatch_logs_client(function) - response = cloudwatch_logs.create_log_group() - response_parser.parse_log_group_creation_response(response, - cloudwatch_logs.get_log_group_name(), - function.get('lambda').get('cli_output')) - - @excp.exception(logger) - def _create_s3_buckets(self, function: Dict): - if function.get('lambda').get('input', False): - s3 = _get_s3_client(function) - for bucket in function.get('lambda').get('input'): - if bucket.get('storage_provider') == 's3': - bucket_name = s3.create_bucket_and_folders(bucket.get('path')) - _get_lambda_client(function).link_function_and_bucket(bucket_name) - s3.set_input_bucket_notification(bucket_name) - - if function.get('lambda').get('output', False): - s3 = _get_s3_client(function) - for bucket in function.get('lambda').get('output'): - if bucket.get('storage_provider') == 's3': - s3.create_bucket_and_folders(bucket.get('path')) - -############################################################################# -### Methods to delete AWS resources ### -############################################################################# - - def _delete_all_resources(self): - for function_info in self._get_all_functions(): - self._delete_resources(function_info) - - def _delete_resources(self, function: Dict) -> None: -# function_name = function_info['FunctionName'] - # Delete associated api - self._delete_api_gateway(function) - # Delete associated log - self._delete_logs(function) -# # Delete associated notifications -# self._delete_bucket_notifications(function_info['FunctionArn'], -# function_info['Environment']['Variables']) - # Delete function - self._delete_lambda_function(function) -# # Delete resources batch -# self._delete_batch_resources(function_name) - - def _delete_api_gateway(self, function): - lambda_client = _get_lambda_client(function) - api_gateway_id = lambda_client.get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') - if api_gateway_id: - response = _get_api_gateway_client(function).delete_api_gateway(api_gateway_id) - response_parser.parse_delete_api_response(response, - api_gateway_id, - function.get('lambda').get('cli_output')) - - def _delete_logs(self, function: Dict): - cloudwatch_logs = _get_cloudwatch_logs_client(function) - log_group_name = cloudwatch_logs.get_log_group_name(function.get('lambda').get('name')) - response = cloudwatch_logs.delete_log_group(log_group_name) - response_parser.parse_delete_log_response(response, - log_group_name, - function.get('lambda').get('cli_output')) - - def _delete_bucket_notifications(self, function_arn, function_env_vars): - s3_provider_id = ""#_get_storage_provider_id('S3', function_env_vars) - input_bucket_id = f'STORAGE_PATH_INPUT_{s3_provider_id}' if s3_provider_id else '' - if input_bucket_id in function_env_vars: - input_path = function_env_vars[input_bucket_id] - input_bucket_name = input_path.split("/", 1)[0] - self.aws_s3.delete_bucket_notification(input_bucket_name, function_arn) - - def _delete_lambda_function(self, function: Dict): - response = _get_lambda_client(function).delete_function(function.get('lambda').get('name')) - response_parser.parse_delete_function_response(response, - function.get('lambda').get('name'), - function.get('lambda').get('cli_output')) - - def _delete_batch_resources(self, function_name): - if self.batch.exist_compute_environments(function_name): - self.batch.delete_compute_environment(function_name) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index eaad3e3c..c391a31f 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -23,14 +23,11 @@ from scar.utils import FileUtils, GitHubUtils, \ GITHUB_USER, GITHUB_SUPERVISOR_PROJECT -def _get_udocker_client(aws: Dict, tmp_payload_folder_path: str, supervisor_zip_path: str) -> Udocker: - return Udocker(aws, tmp_payload_folder_path, supervisor_zip_path) - class FunctionPackager(): """Class to manage the deployment package creation.""" - def __init__(self, aws_properties: Dict): - self._aws = aws_properties + def __init__(self, resources_info: Dict): + self.resources_info = resources_info # Temporal folder to store the supervisor and udocker files self._tmp_payload_folder = FileUtils.create_tmp_dir() # Path where the supervisor is downloaded @@ -52,12 +49,12 @@ def _download_faas_supervisor_zip(self) -> None: supervisor_zip_url = GitHubUtils.get_source_code_url( GITHUB_USER, GITHUB_SUPERVISOR_PROJECT, - self._aws.get('lambda').get('supervisor').get('version')) + self.resources_info.get('lambda').get('supervisor').get('version')) with open(self._supervisor_zip_path, "wb") as thezip: thezip.write(get_file(supervisor_zip_url)) def _extract_handler_code(self) -> None: - function_handler_dest = FileUtils.join_paths(self._tmp_payload_folder.name, f"{self._aws.get('lambda').get('name')}.py") + function_handler_dest = FileUtils.join_paths(self._tmp_payload_folder.name, f"{self.resources_info.get('lambda').get('name')}.py") file_path = "" with ZipFile(self._supervisor_zip_path) as thezip: for file in thezip.namelist(): @@ -74,37 +71,37 @@ def _copy_function_configuration(self): cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") raw_cfg_file = FileUtils.load_config_file() function_cfg = {"storages" : raw_cfg_file.get('storages', {})} - function_cfg.update(self._aws['lambda']) + function_cfg.update(self.resources_info['lambda']) FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): - if self._aws.get('lambda').get('deployment').get('bucket', False) and \ - self._aws.get('lambda').get('container').get('image', False): - _get_udocker_client(self._aws, self._tmp_payload_folder.name, self._supervisor_zip_path).download_udocker_image() - if self._aws.get('lambda').get('image_file'): - _get_udocker_client(self._aws, self._tmp_payload_folder.name, self._supervisor_zip_path).prepare_udocker_image() - del(self._aws['lambda']['image_file']) + if self.resources_info.get('lambda').get('deployment').get('bucket', False) and \ + self.resources_info.get('lambda').get('container').get('image', False): + Udocker(self.resources_info, self._tmp_payload_folder.name, self._supervisor_zip_path).download_udocker_image() + if self.resources_info.get('lambda').get('image_file'): + Udocker(self.resources_info, self._tmp_payload_folder.name, self._supervisor_zip_path).prepare_udocker_image() + del(self.resources_info['lambda']['image_file']) def _add_init_script(self) -> None: """Copy the init script defined by the user to the payload folder.""" - if self._aws.get('lambda').get('init_script', False): - init_script_path = self._aws.get('lambda').get('init_script') + if self.resources_info.get('lambda').get('init_script', False): + init_script_path = self.resources_info.get('lambda').get('init_script') FileUtils.copy_file(init_script_path, FileUtils.join_paths(self._tmp_payload_folder.name, FileUtils.get_file_name(init_script_path))) - del(self._aws['lambda']['init_script']) + del(self.resources_info['lambda']['init_script']) def _add_extra_payload(self) -> None: - if self._aws.get('lambda').get('extra_payload', False): - payload_path = self._aws.get('lambda').get('extra_payload') + if self.resources_info.get('lambda').get('extra_payload', False): + payload_path = self.resources_info.get('lambda').get('extra_payload') logger.info(f"Adding extra payload '{payload_path}'") if FileUtils.is_file(payload_path): - FileUtils.copy_file(self._aws.get('lambda').get('extra_payload'), + FileUtils.copy_file(self.resources_info.get('lambda').get('extra_payload'), self._tmp_payload_folder.name) else: - FileUtils.copy_dir(self._aws.get('lambda').get('extra_payload'), + FileUtils.copy_dir(self.resources_info.get('lambda').get('extra_payload'), self._tmp_payload_folder.name) - del(self._aws['lambda']['extra_payload']) + del(self.resources_info['lambda']['extra_payload']) def _zip_scar_folder(self, lambda_payload_path: str) -> None: """Zips the tmp folder with all the function's files and @@ -115,9 +112,9 @@ def _zip_scar_folder(self, lambda_payload_path: str) -> None: def _check_code_size(self): # Check if the code size fits within the AWS limits - if self._aws.get('lambda').get('deployment').get('bucket', False): + if self.resources_info.get('lambda').get('deployment').get('bucket', False): AWSValidator.validate_s3_code_size(self._tmp_payload_folder.name, - self._aws.get('lambda').get('deployment').get('max_s3_payload_size')) + self.resources_info.get('lambda').get('deployment').get('max_s3_payload_size')) else: AWSValidator.validate_function_code_size(self._tmp_payload_folder.name, - self._aws.get('lambda').get('deployment').get('max_payload_size')) + self.resources_info.get('lambda').get('deployment').get('max_payload_size')) diff --git a/scar/providers/aws/iam.py b/scar/providers/aws/iam.py index c37d3f3d..5130a585 100644 --- a/scar/providers/aws/iam.py +++ b/scar/providers/aws/iam.py @@ -17,8 +17,8 @@ class IAM(GenericClient): - def __init__(self, aws_properties) -> None: - super().__init__(aws_properties.get('iam')) + def __init__(self, resources_info) -> None: + super().__init__(resources_info.get('iam')) def get_user_name_or_id(self): user = self.client.get_user_info() diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 5703b36f..a24f8a14 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -38,21 +38,17 @@ "asynchronous": "False"} -def _get_layers_client(client: LambdaClient, supervisor_version: str) -> LambdaLayers: - return LambdaLayers(client, supervisor_version) - - class Lambda(GenericClient): - def __init__(self, aws_properties: Dict) -> None: - super().__init__(aws_properties.get('lambda', {})) - self.aws = aws_properties - self.function = aws_properties.get('lambda', {}) + def __init__(self, resources_info: Dict) -> None: + super().__init__(resources_info.get('lambda', {})) + self.resources_info = resources_info + self.function = resources_info.get('lambda', {}) def _get_creations_args(self, zip_payload_path: str) -> Dict: return {'FunctionName': self.function.get('name'), 'Runtime': self.function.get('runtime'), - 'Role': self.aws.get('iam').get('role'), + 'Role': self.resources_info.get('iam').get('role'), 'Handler': self.function.get('handler'), 'Code': self._get_function_code(zip_payload_path), 'Environment': self.function.get('environment'), @@ -81,7 +77,7 @@ def create_function(self): return response def _manage_supervisor_layer(self): - layers_client = LambdaLayers(self.function, self.client) + layers_client = LambdaLayers(self.resources_info, self.client) layers_client.check_faas_supervisor_layer() self.function.get('layers', []).append(layers_client.get_latest_supervisor_layer_arn()) @@ -89,10 +85,10 @@ def _manage_supervisor_layer(self): def _get_function_code(self, zip_payload_path: str) -> Dict: '''Zip all the files and folders needed.''' code = {} - FunctionPackager(self.aws).create_zip(zip_payload_path) + FunctionPackager(self.resources_info).create_zip(zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" - S3(self.aws).upload_file(bucket=self.function.get('deployment').get('bucket'), + S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), file_path=zip_payload_path, file_key=file_key) code = {"S3Bucket": self.function.get('deployment_bucket'), @@ -101,8 +97,8 @@ def _get_function_code(self, zip_payload_path: str) -> Dict: code = {"ZipFile": FileUtils.read_file(zip_payload_path, mode="rb")} return code - def delete_function(self, function_name): - return self.client.delete_function(function_name) + def delete_function(self): + return self.client.delete_function(self.function_info.get('lambda').get('name')) def link_function_and_bucket(self, bucket_name: str) -> None: kwargs = {'FunctionName' : self.function.get('name'), @@ -124,9 +120,10 @@ def launch_request_response_event(self, s3_event): return self._launch_s3_event(s3_event) def _launch_s3_event(self, s3_event): - self.aws.lambdaf.payload = s3_event - logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") - return self.launch_lambda_instance() + 'TODO' +# self.aws.lambdaf.payload = s3_event +# logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") +# return self.launch_lambda_instance() def process_asynchronous_lambda_invocations(self, s3_event_list): if (len(s3_event_list) > MAX_CONCURRENT_INVOCATIONS): @@ -142,14 +139,15 @@ def _launch_concurrent_lambda_invocations(self, s3_event_list): pool.close() def launch_lambda_instance(self): - response = self._invoke_lambda_function() - response_args = {'Response' : response, - 'FunctionName' : self.aws.lambdaf.name, - 'OutputType' : self.aws.output, - 'IsAsynchronous' : self.aws.lambdaf.asynchronous} - if hasattr(self.aws, "output_file"): - response_args['OutputFile'] = self.aws.output_file - response_parser.parse_invocation_response(**response_args) + 'TODO' +# response = self._invoke_lambda_function() +# response_args = {'Response' : response, +# 'FunctionName' : self.aws.lambdaf.name, +# 'OutputType' : self.aws.output, +# 'IsAsynchronous' : self.aws.lambdaf.asynchronous} +# if hasattr(self.aws, "output_file"): +# response_args['OutputFile'] = self.aws.output_file +# response_parser.parse_invocation_response(**response_args) def _get_invocation_payload(self): # Default payload @@ -181,20 +179,21 @@ def _set_request_response_call_parameters(self): self.function.update(REQUEST_RESPONSE_CALL) def _update_environment_variables(self, function_info, update_args): - # To update the environment variables we need to retrieve the - # variables defined in lambda and update them with the new values - env_vars = self.aws.lambdaf.environment - if hasattr(self.aws.lambdaf, "environment_variables"): - for env_var in self.aws.lambdaf.environment_variables: - key_val = env_var.split("=") - # Add an specific prefix to be able to find the variables defined by the user - env_vars['Variables']['CONT_VAR_{0}'.format(key_val[0])] = key_val[1] - if hasattr(self.aws.lambdaf, "timeout_threshold"): - env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) - if hasattr(self.aws.lambdaf, "log_level"): - env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level - function_info['Environment']['Variables'].update(env_vars['Variables']) - update_args['Environment'] = function_info['Environment'] + 'TODO' +# # To update the environment variables we need to retrieve the +# # variables defined in lambda and update them with the new values +# env_vars = self.aws.lambdaf.environment +# if hasattr(self.aws.lambdaf, "environment_variables"): +# for env_var in self.aws.lambdaf.environment_variables: +# key_val = env_var.split("=") +# # Add an specific prefix to be able to find the variables defined by the user +# env_vars['Variables']['CONT_VAR_{0}'.format(key_val[0])] = key_val[1] +# if hasattr(self.aws.lambdaf, "timeout_threshold"): +# env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) +# if hasattr(self.aws.lambdaf, "log_level"): +# env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level +# function_info['Environment']['Variables'].update(env_vars['Variables']) +# update_args['Environment'] = function_info['Environment'] def _update_supervisor_layer(self, function_info, update_args): if hasattr(self.aws.lambdaf, "supervisor_layer"): @@ -206,21 +205,22 @@ def _update_supervisor_layer(self, function_info, update_args): update_args['Layers'] = function_layers def update_function_configuration(self, function_info=None): - if not function_info: - function_info = self.get_function_info() - update_args = {'FunctionName' : function_info['FunctionName'] } -# if hasattr(self.aws.lambdaf, "memory"): -# update_args['MemorySize'] = self.aws.lambdaf.memory -# else: -# update_args['MemorySize'] = function_info['MemorySize'] -# if hasattr(self.aws.lambdaf, "time"): -# update_args['Timeout'] = self.aws.lambdaf.time -# else: -# update_args['Timeout'] = function_info['Timeout'] - self._update_environment_variables(function_info, update_args) - self._update_supervisor_layer(function_info, update_args) - self.client.update_function_configuration(**update_args) - logger.info("Function '{}' updated successfully.".format(function_info['FunctionName'])) + 'TODO' +# if not function_info: +# function_info = self.get_function_info() +# update_args = {'FunctionName' : function_info['FunctionName'] } +# # if hasattr(self.aws.lambdaf, "memory"): +# # update_args['MemorySize'] = self.aws.lambdaf.memory +# # else: +# # update_args['MemorySize'] = function_info['MemorySize'] +# # if hasattr(self.aws.lambdaf, "time"): +# # update_args['Timeout'] = self.aws.lambdaf.time +# # else: +# # update_args['Timeout'] = function_info['Timeout'] +# self._update_environment_variables(function_info, update_args) +# self._update_supervisor_layer(function_info, update_args) +# self.client.update_function_configuration(**update_args) +# logger.info("Function '{}' updated successfully.".format(function_info['FunctionName'])) def _get_function_environment_variables(self): return self.get_function_info()['Environment'] @@ -231,9 +231,8 @@ def get_all_functions(self, arn_list): except ClientError as cerr: print (f"Error getting function info by arn: {cerr}") - def get_function_info(self, function_name_or_arn=None): - name_arn = function_name_or_arn if function_name_or_arn else self.function.get('name') - return self.client.get_function_info(name_arn) + def get_function_info(self): + return self.client.get_function_info(self.function.get('name')) @excp.exception(logger) def find_function(self, function_name_or_arn=None): @@ -250,17 +249,17 @@ def find_function(self, function_name_or_arn=None): raise def add_invocation_permission_from_api_gateway(self): - api = self.aws.get('api_gateway') + api = self.resources_info.get('api_gateway') # Add Testing permission kwargs = {'FunctionName': self.function.get('name'), 'Principal': api.get('service_id'), 'SourceArn': api.get('source_arn_testing').format(api_region=api.get('region'), - account_id=self.aws.get('iam').get('account_id'), + account_id=self.resources_info.get('iam').get('account_id'), api_id=api.get('id'))} self.client.add_invocation_permission(**kwargs) # Add Invocation permission kwargs['SourceArn'] = api.get('source_arn_invocation').format(api_region=api.get('region'), - account_id=self.aws.get('iam').get('account_id'), + account_id=self.resources_info.get('iam').get('account_id'), api_id=api.get('id'), stage_name=api.get('stage_name')) self.client.add_invocation_permission(**kwargs) @@ -270,26 +269,29 @@ def get_api_gateway_id(self): return env_vars['Variables'].get('API_GATEWAY_ID', '') def _get_api_gateway_url(self): - api_id = self.get_api_gateway_id() - if not api_id: - raise excp.ApiEndpointNotFoundError(self.aws.lambdaf.name) - return f'https://{api_id}.execute-api.{self.aws.region}.amazonaws.com/scar/launch' + 'TODO' +# api_id = self.get_api_gateway_id() +# if not api_id: +# raise excp.ApiEndpointNotFoundError(self.function.get('name')) +# return f'https://{api_id}.execute-api.{self.resources_info.region}.amazonaws.com/scar/launch' def call_http_endpoint(self): - invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} - if hasattr(self.aws, "api_gateway"): - self._set_invoke_args(invoke_args) - return request.call_http_endpoint(self._get_api_gateway_url(), **invoke_args) + 'TODO' +# invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} +# if hasattr(self.aws, "api_gateway"): +# self._set_invoke_args(invoke_args) +# return request.call_http_endpoint(self._get_api_gateway_url(), **invoke_args) def _set_invoke_args(self, invoke_args): - if hasattr(self.aws.api_gateway, "data_binary"): - invoke_args['data'] = self._get_b64encoded_binary_data(self.aws.api_gateway.data_binary) - invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} - if hasattr(self.aws.api_gateway, "parameters"): - invoke_args['params'] = self._parse_http_parameters(self.aws.api_gateway.parameters) - if hasattr(self.aws.api_gateway, "json_data"): - invoke_args['data'] = self._parse_http_parameters(self.aws.api_gateway.json_data) - invoke_args['headers'] = {'Content-Type': 'application/json'} + 'TODO' +# if hasattr(self.aws.api_gateway, "data_binary"): +# invoke_args['data'] = self._get_b64encoded_binary_data(self.aws.api_gateway.data_binary) +# invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} +# if hasattr(self.aws.api_gateway, "parameters"): +# invoke_args['params'] = self._parse_http_parameters(self.aws.api_gateway.parameters) +# if hasattr(self.aws.api_gateway, "json_data"): +# invoke_args['data'] = self._parse_http_parameters(self.aws.api_gateway.json_data) +# invoke_args['headers'] = {'Content-Type': 'application/json'} def _parse_http_parameters(self, parameters): return parameters if type(parameters) is dict else json.loads(parameters) diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index fa4a7aac..a57b7ba4 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -20,7 +20,7 @@ import zipfile import scar.http.request as request import scar.logger as logger -from scar.utils import lazy_property, FileUtils, GitHubUtils, StrUtils, \ +from scar.utils import FileUtils, GitHubUtils, StrUtils, \ GITHUB_USER, GITHUB_SUPERVISOR_PROJECT from scar.providers.aws.clients.lambdafunction import LambdaClient @@ -64,7 +64,7 @@ def _create_layer_zip(layer_zip_path: str, layer_code_path: str) -> None: class Layer(): """Class used for layer management.""" - def __init__(self, lambda_client) -> None: + def __init__(self, lambda_client: LambdaClient) -> None: self.lambda_client = lambda_client def _find(self, layer_name: str) -> Dict: @@ -103,29 +103,23 @@ def get_latest_layer_info(self, layer_name: str) -> Dict: class LambdaLayers(): """"Class used to manage the lambda supervisor layer.""" - @lazy_property - def layer(self): - """Property used to manage the lambda layers.""" - layer = Layer(self.lambda_client) - return layer - - def __init__(self, function: Dict, lambda_client: LambdaClient) -> None: - self.function = function - self.lambda_client = lambda_client - self.layer_name = self.function.get('supervisor').get('layer_name') - self.supervisor_version = self.function.get('supervisor').get('version') + # To avoid circular inheritance we need to receive the LambdaClient + def __init__(self, resources_info: Dict, lambda_client: LambdaClient): + self.resources_info = resources_info + self.layer_name = self.resources_info.get('supervisor').get('layer_name') + self.supervisor_version = self.resources_info.get('supervisor').get('version') + self.layer = Layer(self.resources_info, lambda_client) def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: return {'LayerName' : self.layer_name, - 'Description' : self.function.get('supervisor').get('version'), + 'Description' : self.supervisor_version, 'Content' : {'ZipFile': FileUtils.read_file(layer_zip_path, mode="rb")}, - 'LicenseInfo' : self.function.get('supervisor').get('license_info')} + 'LicenseInfo' : self.resources_info.get('supervisor').get('license_info')} def _create_layer(self) -> None: tmp_zip_path, layer_code_path = _create_tmp_folders() layer_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), f"{self.layer_name}.zip") - parent_folder = _download_supervisor(self.function.get('supervisor').get('version'), - tmp_zip_path) + parent_folder = _download_supervisor(self.supervisor_version, tmp_zip_path) _copy_supervisor_files(parent_folder, tmp_zip_path, layer_code_path) _copy_extra_files(parent_folder, tmp_zip_path, layer_code_path) _create_layer_zip(layer_zip_path, layer_code_path) diff --git a/scar/providers/aws/launchtemplates.py b/scar/providers/aws/launchtemplates.py index 8dd638c1..9e4afc63 100644 --- a/scar/providers/aws/launchtemplates.py +++ b/scar/providers/aws/launchtemplates.py @@ -30,6 +30,7 @@ 'RequestId': 'XXX', 'RetryAttempts': 0}}""" +from typing import Dict from string import Template from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -42,7 +43,6 @@ class LaunchTemplates(GenericClient): """Class to manage the creation and update of launch templates.""" - _TEMPLATE_NAME = 'faas-supervisor' _SUPERVISOR_GITHUB_REPO = 'faas-supervisor' _SUPERVISOR_GITHUB_USER = 'grycap' _SUPERVISOR_GITHUB_ASSET_NAME = 'supervisor' @@ -55,9 +55,10 @@ class LaunchTemplates(GenericClient): 'chmod +x /opt/faas-supervisor/bin/supervisor\n' ) - def __init__(self, aws_properties, supervisor_version): - super().__init__(aws_properties) - self.supervisor_version = supervisor_version + def __init__(self, resources_info: Dict): + super().__init__(resources_info.get('batch')) + self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version') + self.template_name = resources_info.get('batch').get('compute_resources').get('launch_template_name') if self.supervisor_version == 'latest': self.supervisor_version = GitHubUtils.get_latest_release( self._SUPERVISOR_GITHUB_USER, @@ -69,7 +70,7 @@ def _is_supervisor_created(self) -> bool: """Checks if 'faas-supervisor' launch template is created""" params = {'Filters': [ {'Name': 'launch-template-name', - 'Values': [self._TEMPLATE_NAME]} + 'Values': [self.template_name]} ]} response = self.client.describe_launch_templates(params) return ('LaunchTemplates' in response and @@ -80,12 +81,12 @@ def _is_supervisor_version_created(self) -> int: """Checks if the supervisor version specified is created. Returns the Launch Template version or -1 if it does not exists""" response = self.client.describe_launch_template_versions( - {'LaunchTemplateName': self._TEMPLATE_NAME}) + {'LaunchTemplateName': self.template_name}) versions = response['LaunchTemplateVersions'] # Get ALL versions while ('NextToken' in response and response['NextToken']): response = self.client.describe_launch_template_versions( - {'LaunchTemplateName': self._TEMPLATE_NAME, + {'LaunchTemplateName': self.template_name, 'NextToken': response['NextToken']}) versions.extend(response['LaunchTemplateVersions']) @@ -133,14 +134,14 @@ def get_launch_template_version(self) -> int: if self._is_supervisor_created(): is_created = self._is_supervisor_version_created() if is_created is not -1: - logger.info('Using existent \'faas-supervisor\' launch template.') + logger.info(f"Using existent '{self.template_name}' launch template.") return is_created else: logger.info((f"Creating launch template version with 'faas-supervisor' " f"version '{self.supervisor_version}'.")) user_data = self._create_supervisor_user_data() response = self.client.create_launch_template_version( - self._TEMPLATE_NAME, + self.template_name, self.supervisor_version, {'UserData': user_data}) return response['LaunchTemplateVersion']['VersionNumber'] @@ -149,7 +150,7 @@ def get_launch_template_version(self) -> int: f"version '{self.supervisor_version}'.")) user_data = self._create_supervisor_user_data() response = self.client.create_launch_template( - self._TEMPLATE_NAME, + self.template_name, self.supervisor_version, {'UserData': user_data}) return response['LaunchTemplate']['LatestVersionNumber'] diff --git a/scar/providers/aws/properties.py b/scar/providers/aws/properties.py deleted file mode 100644 index bac05a61..00000000 --- a/scar/providers/aws/properties.py +++ /dev/null @@ -1,191 +0,0 @@ -# # Copyright (C) GRyCAP - I3M - UPV -# # -# # Licensed under the Apache License, Version 2.0 (the "License"); -# # you may not use this file except in compliance with the License. -# # You may obtain a copy of the License at -# # -# # http://www.apache.org/licenses/LICENSE-2.0 -# # -# # Unless required by applicable law or agreed to in writing, software -# # distributed under the License is distributed on an "AS IS" BASIS, -# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# # See the License for the specific language governing permissions and -# # limitations under the License. -# """Module with classes and methods to manage the different -# properties needed by SCAR and the boto clients.""" -# -# -# class ScarProperties(dict): -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# -# -# class AwsProperties(dict): -# -# def __init__(self, *args, **kwargs): -# """ -# {'account_id': '914332', -# 'batch': see_batch_props_class, -# 'boto_profile': 'default', -# 'cloudwatch': see_cloudwatch_props_class, -# 'config_path': 'cowsay', -# 'execution_mode': 'lambda', -# 'iam': see_iam_props_class, -# 'lambda': see_lambdaf_props_class, -# 'output': , -# 'region': 'us-east-1', -# 's3': see_s3_props_class, -# 'tags': {'createdby': 'scar', 'owner': 'alpegon'}} -# """ -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# self._initialize_properties() -# -# def _initialize_properties(self): -# if hasattr(self, "api_gateway"): -# self.api_gateway = ApiGatewayProperties(self.api_gateway) -# if hasattr(self, "batch"): -# self.batch = BatchProperties(self.batch) -# if hasattr(self, "cloudwatch"): -# self.cloudwatch = CloudWatchProperties(self.cloudwatch) -# if hasattr(self, "iam"): -# self.iam = IamProperties(self.iam) -# if hasattr(self, "lambda"): -# self.lambdaf = LambdaProperties(self.__dict__['lambda']) -# self.__dict__.pop('lambda', None) -# if hasattr(self, "s3"): -# self.s3 = S3Properties(self.s3) -# -# -# class ApiGatewayProperties(dict): -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# -# -# class BatchProperties(dict): -# """ -# Example of dictionary used to initialize the class properties: -# {'comp_type': 'EC2', -# 'desired_v_cpus': 0, -# 'instance_types': ['m3.medium'], -# 'max_v_cpus': 2, -# 'min_v_cpus': 0, -# 'security_group_ids': ['sg-2568'], -# 'state': 'ENABLED', -# 'subnets': ['subnet-568', -# 'subnet-569', -# 'subnet-570', -# 'subnet-571', -# 'subnet-572'], -# 'type': 'MANAGED'} -# """ -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# -# -# class LambdaProperties(dict): -# """ -# Example of dictionary used to initialize the class properties: -# {'asynchronous': False, -# 'description': 'Automatically generated lambda function', -# 'environment': {'Variables': {'EXECUTION_MODE': 'lambda', -# 'INPUT_BUCKET': 'test1', -# 'LOG_LEVEL': 'INFO', -# 'SUPERVISOR_TYPE': 'LAMBDA', -# 'TIMEOUT_THRESHOLD': '10', -# 'UDOCKER_BIN': '/opt/udocker/bin/', -# 'UDOCKER_DIR': '/tmp/shared/udocker', -# 'UDOCKER_EXEC': '/opt/udocker/udocker.py', -# 'UDOCKER_LIB': '/opt/udocker/lib/'}}, -# 'extra_payload': '/test/', -# 'handler': 'test.lambda_handler', -# 'image_file': 'minicow.tar.gz', -# 'init_script': 'test.sh', -# 'invocation_type': 'RequestResponse', -# 'layers': ['arn:aws:lambda:us-east-1:914332:layer:faas-supervisor:1'], -# 'log_level': 'INFO', -# 'log_type': 'Tail', -# 'memory': 512, -# 'name': 'test', -# 'runtime': 'python3.6', -# 'time': 300, -# 'timeout_threshold': 10, -# 'zip_file_path': '/tmp/function.zip'} -# """ -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# -# def update_properties(self, **kwargs): -# if 'ResponseMetadata' in kwargs: -# # Parsing RAW function info -# self.description = kwargs['Description'] -# self.environment = kwargs['Environment'] -# self.arn = kwargs['FunctionArn'] -# self.name = kwargs['FunctionName'] -# self.handler = kwargs['Handler'] -# self.layers = [layer['Arn'] for layer in kwargs['Layers']] -# self.memory = kwargs['MemorySize'] -# self.time = kwargs['Timeout'] -# self.role = kwargs['Role'] -# self.runtime = kwargs['Runtime'] -# else: -# self.__dict__.update(**kwargs) -# -# -# class IamProperties(dict): -# """ -# Example of dictionary used to initialize the class properties: -# {'role': 'arn:aws:iam::914332:role/invented-role'} -# """ -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# -# -# class S3Properties(dict): -# """ -# Example of dictionary used to initialize the class properties: -# {'input_bucket': 'test1'} -# """ -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self -# self.process_storage_paths() -# -# def process_storage_paths(self): -# if hasattr(self, "input_bucket"): -# self.storage_path_input = self.input_bucket -# input_path = self.input_bucket.split("/", 1) -# if len(input_path) > 1: -# # There are folders defined -# self.input_bucket = input_path[0] -# self.input_folder = input_path[1] -# -# if hasattr(self, "output_bucket"): -# self.storage_path_output = self.output_bucket -# output_path = self.output_bucket.split("/", 1) -# if len(output_path) > 1: -# # There are folders defined -# self.output_bucket = output_path[0] -# self.output_folder = output_path[1] -# -# -# class CloudWatchProperties(dict): -# """ -# Example of dictionary used to initialize the class properties: -# {'log_retention_policy_in_days': 30} -# """ -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.__dict__ = self diff --git a/scar/providers/aws/resourcegroups.py b/scar/providers/aws/resourcegroups.py index a29eef9f..c0be463c 100644 --- a/scar/providers/aws/resourcegroups.py +++ b/scar/providers/aws/resourcegroups.py @@ -22,8 +22,8 @@ class ResourceGroups(GenericClient): """Class to manage AWS Resource Groups""" - def __init__(self, aws_properties) -> None: - super().__init__(aws_properties.get('lambda')) + def __init__(self, resources_info) -> None: + super().__init__(resources_info.get('lambda')) def get_resource_arn_list(self, iam_user_id: str, resource_type: str = 'lambda') -> List: """Returns a list of ARNs filtered by the resource_type diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index da368134..a729ebfa 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -33,9 +33,9 @@ def _get_bucket_and_folders(storage_path: str) -> Tuple: class S3(GenericClient): - def __init__(self, function_info): + def __init__(self, resources_info): super().__init__() - self.aws = function_info + self.function_info = resources_info @excp.exception(logger) def create_bucket(self, bucket_name) -> None: @@ -64,17 +64,17 @@ def set_input_bucket_notification(self, bucket_name: str) -> None: notification = {"LambdaFunctionConfigurations": lambda_conf} self.client.put_notification_configuration(bucket_name, notification) - def delete_bucket_notification(self, bucket_name, function_arn): + def delete_bucket_notification(self, bucket_name): bucket_conf = self.client.get_notification_configuration(bucket_name) if bucket_conf and "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] - filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != function_arn] + filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.function_info.get('lambda').get('arn')] notification = { "LambdaFunctionConfigurations": filter_conf } self.client.put_notification_configuration(bucket_name, notification) logger.info("Bucket notifications successfully deleted") def get_trigger_configuration(self, bucket_name: str) -> Dict: - return {"LambdaFunctionArn": self.aws.get('lambda').get('arn'), + return {"LambdaFunctionArn": self.function_info.get('lambda').get('arn'), "Events": [ "s3:ObjectCreated:*" ], "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": bucket_name }]}} } @@ -108,19 +108,19 @@ def upload_file(self, bucket: str, folder_name: str =None, file_path: str =None, @excp.exception(logger) def get_bucket_file_list(self): - bucket_name = self.aws.s3.input_bucket + bucket_name = self.function_info.s3.input_bucket if self.client.find_bucket(bucket_name): kwargs = {"Bucket" : bucket_name} - if hasattr(self.aws.s3, "input_folder") and self.aws.s3.input_folder: - kwargs["Prefix"] = self.aws.s3.input_folder + if hasattr(self.function_info.s3, "input_folder") and self.function_info.s3.input_folder: + kwargs["Prefix"] = self.function_info.s3.input_folder return self.client.list_files(**kwargs) else: raise excp.BucketNotFoundError(bucket_name=bucket_name) def get_s3_event(self, s3_file_key): return {"Records": [{"eventSource": "aws:s3", - "s3" : {"bucket" : {"name": self.aws.s3.input_bucket, - "arn": f'arn:aws:s3:::{self.aws.s3.input_bucket}'}, + "s3" : {"bucket" : {"name": self.function_info.s3.input_bucket, + "arn": f'arn:aws:s3:::{self.function_info.s3.input_bucket}'}, "object" : {"key": s3_file_key}}}]} def get_s3_event_list(self, s3_file_keys): diff --git a/scar/providers/aws/udocker.py b/scar/providers/aws/udocker.py index f7cd69d3..6ec4329d 100644 --- a/scar/providers/aws/udocker.py +++ b/scar/providers/aws/udocker.py @@ -29,8 +29,8 @@ def _extract_udocker_zip(supervisor_zip_path) -> None: class Udocker(): - def __init__(self, aws_properties: str, tmp_payload_folder_path: str, supervisor_zip_path: str): - self._aws = aws_properties + def __init__(self, resources_info: str, tmp_payload_folder_path: str, supervisor_zip_path: str): + self.resources_info = resources_info self._tmp_payload_folder_path = tmp_payload_folder_path self._udocker_dir = FileUtils.join_paths(self._tmp_payload_folder_path, "udocker") self._udocker_dir_orig = "" @@ -57,21 +57,21 @@ def _restore_udocker_env(self): SysUtils.delete_environment_variable("UDOCKER_DIR") def _set_udocker_local_registry(self): - self._aws['lambda']['environment']['Variables']['UDOCKER_REPOS'] = '/var/task/udocker/repos/' - self._aws['lambda']['environment']['Variables']['UDOCKER_LAYERS'] = '/var/task/udocker/layers/' + self.resources_info['lambda']['environment']['Variables']['UDOCKER_REPOS'] = '/var/task/udocker/repos/' + self.resources_info['lambda']['environment']['Variables']['UDOCKER_LAYERS'] = '/var/task/udocker/layers/' def _create_udocker_container(self): """Check if the container fits in the limits of the deployment.""" - if self._aws.get('lambda').get('deployment').get('bucket', False): - self._validate_container_size(self._aws.get('lambda').get('deployment').get('max_s3_payload_size')) + if self.resources_info.get('lambda').get('deployment').get('bucket', False): + self._validate_container_size(self.resources_info.get('lambda').get('deployment').get('max_s3_payload_size')) else: - self._validate_container_size(self._aws.get('lambda').get('deployment').get('max_payload_size')) + self._validate_container_size(self.resources_info.get('lambda').get('deployment').get('max_payload_size')) def _validate_container_size(self, max_payload_size): if FileUtils.get_tree_size(self._udocker_dir) < (max_payload_size / 2): - ucmd = self._udocker_exec + ["create", "--name=lambda_cont", self._aws.get('lambda').get('container').get('image')] + ucmd = self._udocker_exec + ["create", "--name=lambda_cont", self.resources_info.get('lambda').get('container').get('image')] SysUtils.execute_command_with_msg(ucmd, cli_msg="Creating container structure") - self._aws['lambda']['environment']['Variables']['UDOCKER_CONTAINERS'] = '/var/task/udocker/containers/' + self.resources_info['lambda']['environment']['Variables']['UDOCKER_CONTAINERS'] = '/var/task/udocker/containers/' elif FileUtils.get_tree_size(self._udocker_dir) > max_payload_size: FileUtils.delete_folder(FileUtils.join_paths(self._udocker_dir, "containers")) @@ -79,7 +79,7 @@ def _validate_container_size(self, max_payload_size): def download_udocker_image(self): self._save_tmp_udocker_env() - SysUtils.execute_command_with_msg(self._udocker_exec + ["pull", self._aws.get('lambda').get('container').get('image')], + SysUtils.execute_command_with_msg(self._udocker_exec + ["pull", self.resources_info.get('lambda').get('container').get('image')], cli_msg="Downloading container image") self._create_udocker_container() self._set_udocker_local_registry() @@ -88,11 +88,11 @@ def download_udocker_image(self): def prepare_udocker_image(self): self._save_tmp_udocker_env() image_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), "udocker_image.tar.gz") - FileUtils.copy_file(self._aws.get('lambda').get('image_file'), image_path) + FileUtils.copy_file(self.resources_info.get('lambda').get('image_file'), image_path) cmd_out = SysUtils.execute_command_with_msg(self._udocker_exec + ["load", "-i", image_path], cli_msg="Loading image file") # Get the image name from the command output - self._aws['lambda']['container']['image'] = cmd_out.split('\n')[1] + self.resources_info['lambda']['container']['image'] = cmd_out.split('\n')[1] self._create_udocker_container() self._set_udocker_local_registry() self._restore_udocker_env() From 5d3468509ce8618067117bafa375ed60e4a4b48d Mon Sep 17 00:00:00 2001 From: Alfonso Date: Fri, 22 Nov 2019 17:55:24 +0100 Subject: [PATCH 10/41] WIP - Add get and put / Fix several bugs --- scar/parser/cli/__init__.py | 4 +- scar/parser/cli/subparsers.py | 4 - scar/providers/aws/batchfunction.py | 4 +- scar/providers/aws/controller.py | 511 +++++++++++++-------------- scar/providers/aws/functioncode.py | 3 +- scar/providers/aws/lambdafunction.py | 62 ++-- scar/providers/aws/lambdalayers.py | 6 +- scar/providers/aws/response.py | 39 +- scar/providers/aws/s3.py | 46 +-- scar/scarcli.py | 2 +- scar/utils.py | 4 +- 11 files changed, 341 insertions(+), 344 deletions(-) diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index 50cbef4f..b6b38177 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -76,13 +76,13 @@ def _parse_lambda_args(cmd_args: Dict) -> Dict: def _get_lambda_environment_variables(lambda_args: Dict) -> None: lambda_env_vars = {"environment": {"Variables": {}}, - "container": {'environment_variables' : {}}} + "container": {'environment' : {"Variables": {}}}} if "environment_variables" in lambda_args: # These variables define the udocker container environment variables for env_var in lambda_args["environment_variables"]: key_val = env_var.split("=") # Add an specific prefix to be able to find the container variables defined by the user - lambda_env_vars['container']['environment_variables'][f'{key_val[0]}'] = key_val[1] + lambda_env_vars['container']['environment']['Variables'][f'{key_val[0]}'] = key_val[1] del(lambda_args['environment_variables']) if "extra_payload" in lambda_args: lambda_env_vars['container']['extra_payload'] = f"/var/task" diff --git a/scar/parser/cli/subparsers.py b/scar/parser/cli/subparsers.py index 90a2146d..867da616 100644 --- a/scar/parser/cli/subparsers.py +++ b/scar/parser/cli/subparsers.py @@ -154,10 +154,6 @@ def _add_ls_parser(self): ls.set_defaults(func='ls') # S3 args ls.add_argument("-b", "--bucket", help="Show bucket files") - # Layer args - ls.add_argument("-l", "--list-layers", - help="Show lambda layers information", - action="store_true") def _add_put_parser(self): put = self.subparser.add_parser('put', diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index d6b671d2..5d446292 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -35,8 +35,8 @@ def __init__(self, resources_info): def _set_required_environment_variables(self) -> None: self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self.function_name) self._set_batch_environment_variable('SCRIPT', self._get_user_script()) - if self.resources_info.get('lambda').get('container').get('environment_variables', False): - for key, value in self.resources_info.get('lambda').get('container').get('environment_variables').items(): + if self.resources_info.get('lambda').get('container').get('environment').get('Variables', False): + for key, value in self.resources_info.get('lambda').get('container').get('environment').get('Variables').items(): self._set_batch_environment_variable(key, value) def _set_batch_environment_variable(self, key: str, value: str) -> None: diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 46cf32ad..15954eed 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -23,7 +23,7 @@ from scar.providers.aws.lambdafunction import Lambda # from scar.providers.aws.properties import AwsProperties, ScarProperties from scar.providers.aws.resourcegroups import ResourceGroups -from scar.providers.aws.s3 import S3 +from scar.providers.aws.s3 import S3, get_bucket_and_folders from scar.providers.aws.validators import AWSValidator import scar.exceptions as excp import scar.logger as logger @@ -32,191 +32,82 @@ _ACCOUNT_ID_REGEX = r'\d{12}' -# def _get_storage_provider_id(storage_provider: str, env_vars: Dict) -> str: -# """Searches the storage provider id in the environment variables: -# get_provider_id(S3, {'STORAGE_AUTH_S3_USER_41807' : 'scar'}) -# returns -> 41807""" -# res = "" -# for env_key in env_vars.keys(): -# if env_key.startswith(f'STORAGE_AUTH_{storage_provider}'): -# res = env_key.split('_', 4)[-1] -# break -# return res - def _get_owner(resources_info: Dict): return IAM(resources_info).get_user_name_or_id() + def _check_function_defined(resources_info: Dict): if Lambda(resources_info).find_function(): raise excp.FunctionExistsError(function_name=resources_info.get('lambda', {}).get('name', '')) + def _check_function_not_defined(resources_info: Dict): if not Lambda(resources_info).find_function(): raise excp.FunctionNotFoundError(function_name=resources_info.get('lambda', {}).get('name', '')) + +def _choose_function(aws_resources: Dict) -> int: + function_names = [resources_info.get('lambda').get('name') for resources_info in aws_resources] + print("Please choose a function:") + print("0) Apply to all") + for idx, element in enumerate(function_names): + print(f"{idx+1}) {element}") + i = input("Enter number: ") + if 0 < int(i) <= len(function_names): + return int(i) - 1 + return None + ############################################ ### ADD EXTRA PROPERTIES ### ############################################ -def _add_extra_aws_properties(scar: Dict, aws_functions: Dict) -> None: - for function in aws_functions: - _add_tags(function) - _add_handler(function) - _add_account_id(function) - _add_output(scar, function) - _add_config_file_path(scar, function) +def _add_extra_aws_properties(scar: Dict, aws_resources: Dict) -> None: + for resources_info in aws_resources: + _add_tags(resources_info) + _add_handler(resources_info) + _add_account_id(resources_info) + _add_output(scar) + _add_config_file_path(scar, resources_info) -def _add_tags(function: Dict): - function['lambda']['tags'] = {"createdby": "scar", - "owner": _get_owner(function)} +def _add_tags(resources_info: Dict): + resources_info['lambda']['tags'] = {"createdby": "scar", "owner": _get_owner(resources_info)} -def _add_account_id(function: Dict): - function['iam']['account_id'] = StrUtils.find_expression(function['iam']['role'], - _ACCOUNT_ID_REGEX) +def _add_account_id(resources_info: Dict): + resources_info['iam']['account_id'] = StrUtils.find_expression(resources_info['iam']['role'], _ACCOUNT_ID_REGEX) -def _add_handler(function: Dict): - function['lambda']['handler'] = f"{function.get('lambda', {}).get('name', '')}.lambda_handler" +def _add_handler(resources_info: Dict): + resources_info['lambda']['handler'] = f"{resources_info.get('lambda').get('name')}.lambda_handler" -def _add_output(scar_props: Dict, function: Dict): - function['lambda']['cli_output'] = response_parser.OutputType.PLAIN_TEXT.value - if scar_props.get("json", False): - function['lambda']['cli_output'] = response_parser.OutputType.JSON.value +def _add_output(scar_info: Dict): + scar_info['cli_output'] = response_parser.OutputType.PLAIN_TEXT.value + if scar_info.get("json", False): + scar_info['cli_output'] = response_parser.OutputType.JSON.value # Override json ouput if both of them are defined - if scar_props.get("verbose", False): - function['lambda']['cli_output'] = response_parser.OutputType.VERBOSE.value - if scar_props.get("output_file", False): - function['lambda']['cli_output'] = response_parser.OutputType.BINARY.value - function['lambda']['output_file'] = scar_props.get("output_file") + if scar_info.get("verbose", False): + scar_info['cli_output'] = response_parser.OutputType.VERBOSE.value + if scar_info.get("output_file", False): + scar_info['cli_output'] = response_parser.OutputType.BINARY.value -def _add_config_file_path(scar_props: Dict, function: Dict): - if scar_props.get("conf_file", False): - function['lambda']['config_path'] = os.path.dirname(scar_props.get("conf_file")) +def _add_config_file_path(scar_info: Dict, resources_info: Dict): + if scar_info.get("conf_file", False): + resources_info['lambda']['config_path'] = os.path.dirname(scar_info.get("conf_file")) # Update the path of the files based on the path of the yaml (if any) - if function['lambda'].get('init_script', False): - function['lambda']['init_script'] = FileUtils.join_paths(function['lambda']['config_path'], - function['lambda']['init_script']) - if function['lambda'].get('image_file', False): - function['lambda']['image_file'] = FileUtils.join_paths(function['lambda']['config_path'], - function['lambda']['image_file']) - if function['lambda'].get('run_script', False): - function['lambda']['run_script'] = FileUtils.join_paths(function['lambda']['config_path'], - function['lambda']['run_script']) - -############################################################################# -### Methods to create AWS resources ### -############################################################################# - -@excp.exception(logger) -def _create_api_gateway(resources_info: Dict): - if resources_info.get("api_gateway", {}).get('name', False): - APIGateway(resources_info).create_api_gateway() - -@excp.exception(logger) -def _create_lambda_function(resources_info: Dict) -> None: - lambda_client = Lambda(resources_info) - response = lambda_client.create_function() - response_parser.parse_lambda_function_creation_response(response, - resources_info, - lambda_client.get_access_key()) - -@excp.exception(logger) -def _create_log_group(resources_info: Dict) -> None: - cloudwatch_logs = CloudWatchLogs(resources_info) - response = cloudwatch_logs.create_log_group() - response_parser.parse_log_group_creation_response(response, - cloudwatch_logs.get_log_group_name(), - resources_info.get('lambda').get('cli_output')) - -@excp.exception(logger) -def _create_s3_buckets(resources_info: Dict) -> None: - if resources_info.get('lambda').get('input', False): - s3 = S3(resources_info) - for bucket in resources_info.get('lambda').get('input'): - if bucket.get('storage_provider') == 's3': - bucket_name = s3.create_bucket_and_folders(bucket.get('path')) - Lambda(resources_info).link_function_and_bucket(bucket_name) - s3.set_input_bucket_notification(bucket_name) - - if resources_info.get('lambda').get('output', False): - s3 = Lambda(resources_info) - for bucket in resources_info.get('lambda').get('output'): - if bucket.get('storage_provider') == 's3': - s3.create_bucket_and_folders(bucket.get('path')) - -@excp.exception(logger) -def _add_api_gateway_permissions(resources_info: Dict): - if resources_info.get("api_gateway").get('name', False): - Lambda(resources_info).add_invocation_permission_from_api_gateway() - -@excp.exception(logger) -def _create_batch_environment(resources_info: Dict) -> None: - mode = resources_info.get('lambda').get('execution_mode') - if mode == "batch" or mode == "lambda-batch": - Batch(resources_info).create_batch_environment() - -############################################################################# -### Methods to delete AWS resources ### -############################################################################# - -def _delete_all_resources(): - 'TODO' -# for function_info in self._get_all_functions(): -# self._delete_resources(function_info) - -def _delete_resources(resources_info: Dict) -> None: - # Delete associated api - _delete_api_gateway(resources_info) - # Delete associated log - _delete_logs(resources_info) - # Delete associated notifications - _delete_bucket_notifications(resources_info) - # Delete function - _delete_lambda_function(resources_info) - # Delete resources batch - _delete_batch_resources(resources_info) - -def _delete_api_gateway(resources_info: Dict) -> None: - api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') - if api_gateway_id: - resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id - response = APIGateway(resources_info).delete_api_gateway() - response_parser.parse_delete_api_response(response, - api_gateway_id, - resources_info.get('lambda').get('cli_output')) - -def _delete_logs(resources_info: Dict): - cloudwatch_logs = CloudWatchLogs(resources_info) - log_group_name = cloudwatch_logs.get_log_group_name(resources_info.get('lambda').get('name')) - response = cloudwatch_logs.delete_log_group(log_group_name) - response_parser.parse_delete_log_response(response, - log_group_name, - resources_info.get('lambda').get('cli_output')) - -def _delete_bucket_notifications(resources_info: Dict) -> None: - if resources_info.get('lambda').get('input', False): - for input_storage in resources_info.get('lambda').get('input'): - if input_storage.get('storage_provider') == 's3': - bucket_name = input_storage.get('path').split("/", 1)[0] - S3(resources_info).delete_bucket_notification(bucket_name) - -def _delete_lambda_function(resources_info: Dict) -> None: - response = Lambda(resources_info).delete_function() - response_parser.parse_delete_function_response(response, - resources_info.get('lambda').get('name'), - resources_info.get('lambda').get('cli_output')) - -def _delete_batch_resources(resources_info: Dict) -> None: - batch = Batch(resources_info) - if batch.exist_compute_environments(): - batch.delete_compute_environment() - + if resources_info['lambda'].get('init_script', False): + resources_info['lambda']['init_script'] = FileUtils.join_paths(resources_info['lambda']['config_path'], + resources_info['lambda']['init_script']) + if resources_info['lambda'].get('image_file', False): + resources_info['lambda']['image_file'] = FileUtils.join_paths(resources_info['lambda']['config_path'], + resources_info['lambda']['image_file']) + if resources_info['lambda'].get('run_script', False): + resources_info['lambda']['run_script'] = FileUtils.join_paths(resources_info['lambda']['config_path'], + resources_info['lambda']['run_script']) ############################################ ### AWS CONTROLLER ### @@ -228,12 +119,12 @@ class AWS(Commands): Used to manage all the AWS calls and functionalities.""" def __init__(self, func_call): - self.raw_args = FileUtils.load_config_file() + self.raw_args = FileUtils.load_tmp_config_file() self.validate_arguments(self.raw_args) - self.aws_functions = self.raw_args.get('functions', {}).get('aws', {}) + self.aws_resources = self.raw_args.get('functions', {}).get('aws', {}) self.storages = self.raw_args.get('storages', {}) - self.scar = self.raw_args.get('scar', {}) - _add_extra_aws_properties(self.scar, self.aws_functions) + self.scar_info = self.raw_args.get('scar', {}) + _add_extra_aws_properties(self.scar_info, self.aws_resources) # Call the user's command getattr(self, func_call)() @@ -248,17 +139,17 @@ def validate_arguments(self, merged_args: Dict) -> None: @excp.exception(logger) def init(self) -> None: - # supervisor_version = self.scar.get('supervisor_version', 'latest') - for resources_info in self.aws_functions: + # supervisor_version = self.scar_info.get('supervisor_version', 'latest') + for resources_info in self.aws_resources: _check_function_defined(resources_info) # We have to create the gateway before creating the function - _create_api_gateway(resources_info) - _create_lambda_function(resources_info) - _create_log_group(resources_info) - _create_s3_buckets(resources_info) + self._create_api_gateway(resources_info) + self._create_lambda_function(resources_info) + self._create_log_group(resources_info) + self._create_s3_buckets(resources_info) # The api_gateway permissions are added after the function is created - _add_api_gateway_permissions(resources_info) - _create_batch_environment() + self._add_api_gateway_permissions(resources_info) + self._create_batch_environment(resources_info) # self._preheat_function() @excp.exception(logger) @@ -274,18 +165,17 @@ def invoke(self): @excp.exception(logger) def run(self): - resources_info = self.aws_functions[0] - lambda_client = Lambda(resources_info) - if lambda_client.is_asynchronous(): - lambda_client.set_asynchronous_call_parameters() - lambda_client.launch_lambda_instance() + index = 0 + if len(self.aws_resources) > 1: + index = _choose_function(self.aws_resources) + resources_info = self.aws_resources[index] + response = Lambda(resources_info).launch_lambda_instance() + if self.scar_info.get("output_file", False): + response['OutputFile'] = self.scar_info.get("output_file") + response_parser.parse_invocation_response(**response) 'TODO FINISH' # if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): # self._process_input_bucket_calls() -# else: -# if self.aws_lambda.is_asynchronous(): -# self.aws_lambda.set_asynchronous_call_parameters() -# self.aws_lambda.launch_lambda_instance() @excp.exception(logger) def update(self): @@ -297,42 +187,34 @@ def update(self): @excp.exception(logger) def ls(self): - lambda_functions = self._get_all_functions() - response_parser.parse_ls_response(lambda_functions, - self.aws_functions[0].get('lambda').get('cli_output')) - 'TODO FINISH' -# if self.storages: -# file_list = _get_s3_client(self.aws_functions).get_bucket_file_list() -# for file_info in file_list: -# print(file_info) -# else: -# lambda_functions = self._get_all_functions() -# response_parser.parse_ls_response(lambda_functions, -# self.aws_properties.output) + # If a bucket is defined, then we list their files + resources_info = self.aws_resources[0] + if resources_info.get('lambda').get('input', False): + file_list = S3(resources_info).get_bucket_file_list() + for file_info in file_list: + print(file_info) + else: + aws_resources = self._get_all_functions() + response_parser.parse_ls_response(aws_resources, self.scar_info.get('cli_output')) @excp.exception(logger) def rm(self): - 'TODO FINISH' -# function_info = _get_lambda_client(self.aws_functions[0]).get_function_info(self.aws_functions[0]['lambda']['name']) -# self._delete_resources(function_info) - if self.scar.get('all', False): - "Delete all functions" - elif len(self.aws_functions) > 1: - "Please select the function to delete" + if self.scar_info.get('all', False): + for resources_info in self._get_all_functions(): + self._delete_resources(resources_info) else: - "Delete selected function" - resources_info = self.aws_functions[0] - _check_function_not_defined(resources_info) - self._delete_resources(resources_info) - -# function = self.aws_functions[0] -# lambda_client = _get_lambda_client(function) -# if not lambda_client.find_function(function['lambda']['name']): -# raise excp.FunctionNotFoundError(function_name=function['lambda']['name']) - # self._delete_resources(function_info) -# if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: -# self._delete_all_resources() -# else: + index = 0 + if len(self.aws_resources) > 1: + index = _choose_function(self.aws_resources) + # -1 means apply to all functions + if index == -1: + for resources_info in self.aws_resources: + _check_function_not_defined(resources_info) + self._delete_resources(resources_info) + else: + resources_info = self.aws_resources[index] + _check_function_not_defined(resources_info) + self._delete_resources(resources_info) @excp.exception(logger) def log(self): @@ -344,42 +226,147 @@ def log(self): @excp.exception(logger) def put(self): - 'TODO' -# self.upload_file_or_folder_to_s3() + self._upload_file_or_folder_to_s3(self.aws_resources[0]) @excp.exception(logger) def get(self): - 'TODO' -# self.download_file_or_folder_from_s3() + self._download_file_or_folder_from_s3(self.aws_resources[0]) + +############################################################################# +### Methods to create AWS resources ### +############################################################################# + + @excp.exception(logger) + def _create_api_gateway(self, resources_info: Dict): + if resources_info.get("api_gateway", {}).get('name', False): + APIGateway(resources_info).create_api_gateway() + + + @excp.exception(logger) + def _create_lambda_function(self, resources_info: Dict) -> None: + lambda_client = Lambda(resources_info) + response = lambda_client.create_function() + response_parser.parse_lambda_function_creation_response(response, + self.scar_info.get('cli_output'), + lambda_client.get_access_key()) + + + @excp.exception(logger) + def _create_log_group(self, resources_info: Dict) -> None: + cloudwatch_logs = CloudWatchLogs(resources_info) + response = cloudwatch_logs.create_log_group() + response_parser.parse_log_group_creation_response(response, + cloudwatch_logs.get_log_group_name(), + self.scar_info.get('cli_output')) + + + @excp.exception(logger) + def _create_s3_buckets(self, resources_info: Dict) -> None: + if resources_info.get('lambda').get('input', False): + s3_service = S3(resources_info) + for bucket in resources_info.get('lambda').get('input'): + if bucket.get('storage_provider') == 's3': + bucket_name, _ = s3_service.create_bucket_and_folders(bucket.get('path')) + Lambda(resources_info).link_function_and_bucket(bucket_name) + s3_service.set_input_bucket_notification(bucket_name) + + if resources_info.get('lambda').get('output', False): + s3_service = Lambda(resources_info) + for bucket in resources_info.get('lambda').get('output'): + if bucket.get('storage_provider') == 's3': + s3_service.create_bucket_and_folders(bucket.get('path')) + + + @excp.exception(logger) + def _add_api_gateway_permissions(self, resources_info: Dict): + if resources_info.get("api_gateway").get('name', False): + Lambda(resources_info).add_invocation_permission_from_api_gateway() + + + @excp.exception(logger) + def _create_batch_environment(self, resources_info: Dict) -> None: + mode = resources_info.get('lambda').get('execution_mode') + if mode in ("batch", "lambda-batch"): + Batch(resources_info).create_batch_environment() + +############################################################################# +### Methods to delete AWS resources ### +############################################################################# + + def _delete_resources(self, resources_info: Dict) -> None: + # Delete associated api + self._delete_api_gateway(resources_info) + # Delete associated log + self._delete_logs(resources_info) + # Delete associated notifications + self._delete_bucket_notifications(resources_info) + # Delete function + self._delete_lambda_function(resources_info) + # Delete resources batch + self._delete_batch_resources(resources_info) + + + def _delete_api_gateway(self, resources_info: Dict) -> None: + api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') + if api_gateway_id: + resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id + response = APIGateway(resources_info).delete_api_gateway() + response_parser.parse_delete_api_response(response, + api_gateway_id, + self.scar_info.get('cli_output')) + + + def _delete_logs(self, resources_info: Dict): + cloudwatch_logs = CloudWatchLogs(resources_info) + log_group_name = cloudwatch_logs.get_log_group_name(resources_info.get('lambda').get('name')) + response = cloudwatch_logs.delete_log_group(log_group_name) + response_parser.parse_delete_log_response(response, + log_group_name, + self.scar_info.get('cli_output')) + + + def _delete_bucket_notifications(self, resources_info: Dict) -> None: + if resources_info.get('lambda').get('input', False): + for input_storage in resources_info.get('lambda').get('input'): + if input_storage.get('storage_provider') == 's3': + bucket_name = input_storage.get('path').split("/", 1)[0] + S3(resources_info).delete_bucket_notification(bucket_name) + + + def _delete_lambda_function(self, resources_info: Dict) -> None: + response = Lambda(resources_info).delete_function() + response_parser.parse_delete_function_response(response, + resources_info.get('lambda').get('name'), + self.scar_info.get('cli_output')) + + + def _delete_batch_resources(self, resources_info: Dict) -> None: + batch = Batch(resources_info) + if batch.exist_compute_environments(): + batch.delete_compute_environment() ############################################ ### ------------ ### ############################################ def _get_all_functions(self): - # There can be several functions defined - # We check all and merge their arns to list them - -# for function in self.aws_functions: -# iam_info = _get_iam_client(function).get_user_name_or_id() -# functions_arn.union(set(_get_resource_groups_client(function).get_resource_arn_list(iam_info))) -# return _get_lambda_client(self.aws_functions[0]).get_all_functions(functions_arn) - resources_info = self.aws_functions[0] + # Return the resources of the region in the scar's configuration file + resources_info = self.aws_resources[0] arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id()) return Lambda(resources_info).get_all_functions(arn_list) def _get_batch_logs(self) -> str: logs = "" - if hasattr(self.aws_properties.cloudwatch, "request_id") and \ - self.batch.exist_job(self.aws_properties.cloudwatch.request_id): - batch_jobs = self.batch.describe_jobs(self.aws_properties.cloudwatch.request_id) - logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) +# if hasattr(self.aws_properties.cloudwatch, "request_id") and \ +# self.batch.exist_job(self.aws_properties.cloudwatch.request_id): +# batch_jobs = self.batch.describe_jobs(self.aws_properties.cloudwatch.request_id) +# logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) return logs - def _preheat_function(self): - # If preheat is activated, the function is launched at the init step - if hasattr(self.scar, "preheat"): - self.aws_lambda.preheat_function() +# def _preheat_function(self): +# # If preheat is activated, the function is launched at the init step +# if hasattr(self.scar, "preheat"): +# self.aws_lambda.preheat_function() def _process_input_bucket_calls(self): s3_file_list = self.aws_s3.get_bucket_file_list() @@ -393,35 +380,6 @@ def _process_input_bucket_calls(self): s3_event_list = self.aws_s3.get_s3_event_list(s3_file_list) self.aws_lambda.process_asynchronous_lambda_invocations(s3_event_list) - def upload_file_or_folder_to_s3(self): - path_to_upload = self.scar.path - self.aws_s3.create_input_bucket() - files = [path_to_upload] - if os.path.isdir(path_to_upload): - files = FileUtils.get_all_files_in_directory(path_to_upload) - for file_path in files: - self.aws_s3.upload_file(folder_name=self.aws_properties.s3.input_folder, - file_path=file_path) - - def _get_download_file_path(self, file_key=None): - file_path = file_key - if hasattr(self.scar, "path") and self.scar.path: - file_path = FileUtils.join_paths(self.scar.path, file_path) - return file_path - - def download_file_or_folder_from_s3(self): - bucket_name = self.aws_properties.s3.input_bucket - s3_file_list = self.aws_s3.get_bucket_file_list() - for s3_file in s3_file_list: - # Avoid download s3 'folders' - if not s3_file.endswith('/'): - file_path = self._get_download_file_path(file_key=s3_file) - # make sure the path folders are created - dir_path = os.path.dirname(file_path) - if dir_path and not os.path.isdir(dir_path): - os.makedirs(dir_path, exist_ok=True) - self.aws_s3.download_file(bucket_name, s3_file, file_path) - def _update_all_functions(self, lambda_functions): for function_info in lambda_functions: self.aws_lambda.update_function_configuration(function_info) @@ -439,3 +397,38 @@ def _update_local_function_properties(self, function_info): # else: # self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) +####################################################### +### Methods to manage S3 files ### +####################################################### + + def _upload_file_or_folder_to_s3(self, resources_info: Dict) -> None: + path_to_upload = self.scar_info.get('path') + files = [path_to_upload] + if os.path.isdir(path_to_upload): + files = FileUtils.get_all_files_in_directory(path_to_upload) + s3_service = S3(resources_info) + storage_path = resources_info.get('lambda').get('input')[0].get('path') + bucket, folder = s3_service.create_bucket_and_folders(storage_path) + for file_path in files: + s3_service.upload_file(bucket=bucket, folder_name=folder, file_path=file_path) + + def _get_download_file_path(self, file_key=None): + file_path = file_key + if self.scar_info.get('path', False): + file_path = FileUtils.join_paths(self.scar_info.get('path'), file_path) + return file_path + + def _download_file_or_folder_from_s3(self, resources_info: Dict) -> None: + + s3_service = S3(resources_info) + s3_file_list = s3_service.get_bucket_file_list() + for s3_file in s3_file_list: + # Avoid download s3 'folders' + if not s3_file.endswith('/'): + file_path = self._get_download_file_path(file_key=s3_file) + # make sure the path folders are created + dir_path = os.path.dirname(file_path) + if dir_path and not os.path.isdir(dir_path): + os.makedirs(dir_path, exist_ok=True) + bucket, _ = get_bucket_and_folders(resources_info.get('lambda').get('input')[0].get('path')) + s3_service.download_file(bucket, s3_file, file_path) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index c391a31f..7c859e04 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -69,8 +69,7 @@ def _extract_handler_code(self) -> None: def _copy_function_configuration(self): cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") - raw_cfg_file = FileUtils.load_config_file() - function_cfg = {"storages" : raw_cfg_file.get('storages', {})} + function_cfg = {"storages" : FileUtils.load_tmp_config_file().get('storages', {})} function_cfg.update(self.resources_info['lambda']) FileUtils.write_yaml(cfg_file_path, function_cfg) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index a24f8a14..b8510df6 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -19,15 +19,13 @@ from scar.providers.aws import GenericClient from scar.providers.aws.functioncode import FunctionPackager from scar.providers.aws.lambdalayers import LambdaLayers -from scar.providers.aws.clients.lambdafunction import LambdaClient from scar.providers.aws.s3 import S3 from scar.providers.aws.validators import AWSValidator import scar.exceptions as excp -import scar.http.request as request import scar.logger as logger -import scar.providers.aws.response as response_parser from scar.utils import DataTypesUtils, FileUtils, StrUtils from typing import Dict +from scar.parser.cfgfile import ConfigFileParser MAX_CONCURRENT_INVOCATIONS = 500 ASYNCHRONOUS_CALL = {"invocation_type": "Event", @@ -98,7 +96,7 @@ def _get_function_code(self, zip_payload_path: str) -> Dict: return code def delete_function(self): - return self.client.delete_function(self.function_info.get('lambda').get('name')) + return self.client.delete_function(self.resources_info.get('lambda').get('name')) def link_function_and_bucket(self, bucket_name: str) -> None: kwargs = {'FunctionName' : self.function.get('name'), @@ -139,15 +137,14 @@ def _launch_concurrent_lambda_invocations(self, s3_event_list): pool.close() def launch_lambda_instance(self): - 'TODO' -# response = self._invoke_lambda_function() -# response_args = {'Response' : response, -# 'FunctionName' : self.aws.lambdaf.name, -# 'OutputType' : self.aws.output, -# 'IsAsynchronous' : self.aws.lambdaf.asynchronous} -# if hasattr(self.aws, "output_file"): -# response_args['OutputFile'] = self.aws.output_file -# response_parser.parse_invocation_response(**response_args) + if self.is_asynchronous(): + self.set_asynchronous_call_parameters() + response = self._invoke_lambda_function() + response_args = {'Response' : response, + 'FunctionName' : self.function.get('name'), + 'OutputType' : self.function.get('cli_output'), + 'IsAsynchronous' : self.function.get('asynchronous')} + return response_args def _get_invocation_payload(self): # Default payload @@ -192,8 +189,8 @@ def _update_environment_variables(self, function_info, update_args): # env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) # if hasattr(self.aws.lambdaf, "log_level"): # env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level -# function_info['Environment']['Variables'].update(env_vars['Variables']) -# update_args['Environment'] = function_info['Environment'] +# resources_info['Environment']['Variables'].update(env_vars['Variables']) +# update_args['Environment'] = resources_info['Environment'] def _update_supervisor_layer(self, function_info, update_args): if hasattr(self.aws.lambdaf, "supervisor_layer"): @@ -206,33 +203,46 @@ def _update_supervisor_layer(self, function_info, update_args): def update_function_configuration(self, function_info=None): 'TODO' -# if not function_info: -# function_info = self.get_function_info() -# update_args = {'FunctionName' : function_info['FunctionName'] } +# if not resources_info: +# resources_info = self.get_function_info() +# update_args = {'FunctionName' : resources_info['FunctionName'] } # # if hasattr(self.aws.lambdaf, "memory"): # # update_args['MemorySize'] = self.aws.lambdaf.memory # # else: -# # update_args['MemorySize'] = function_info['MemorySize'] +# # update_args['MemorySize'] = resources_info['MemorySize'] # # if hasattr(self.aws.lambdaf, "time"): # # update_args['Timeout'] = self.aws.lambdaf.time # # else: -# # update_args['Timeout'] = function_info['Timeout'] -# self._update_environment_variables(function_info, update_args) -# self._update_supervisor_layer(function_info, update_args) +# # update_args['Timeout'] = resources_info['Timeout'] +# self._update_environment_variables(resources_info, update_args) +# self._update_supervisor_layer(resources_info, update_args) # self.client.update_function_configuration(**update_args) -# logger.info("Function '{}' updated successfully.".format(function_info['FunctionName'])) +# logger.info("Function '{}' updated successfully.".format(resources_info['FunctionName'])) def _get_function_environment_variables(self): return self.get_function_info()['Environment'] + def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict: + result = ConfigFileParser().get_properties().get('aws') + result['lambda']['name'] = aws_conf['FunctionName'] + result['lambda']['arn'] = aws_conf['FunctionArn'] + result['lambda']['timeout'] = aws_conf['Timeout'] + result['lambda']['memory'] = aws_conf['MemorySize'] + result['lambda']['environment']['Variables'] = aws_conf['Environment']['Variables'].copy() + result['lambda']['layers'] = aws_conf['Layers'].copy() + result['lambda']['supervisor']['version'] = aws_conf['SupervisorVersion'] + return result + def get_all_functions(self, arn_list): try: - return [self.get_function_info(function_arn) for function_arn in arn_list] + return [self.merge_aws_and_local_configuration(self.get_function_info(function_arn)) + for function_arn in arn_list] except ClientError as cerr: print (f"Error getting function info by arn: {cerr}") - def get_function_info(self): - return self.client.get_function_info(self.function.get('name')) + def get_function_info(self, arn: str =None) -> Dict: + function = arn if arn else self.function.get('name') + return self.client.get_function_info(function) @excp.exception(logger) def find_function(self, function_name_or_arn=None): diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index a57b7ba4..6ef721b0 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -106,9 +106,9 @@ class LambdaLayers(): # To avoid circular inheritance we need to receive the LambdaClient def __init__(self, resources_info: Dict, lambda_client: LambdaClient): self.resources_info = resources_info - self.layer_name = self.resources_info.get('supervisor').get('layer_name') - self.supervisor_version = self.resources_info.get('supervisor').get('version') - self.layer = Layer(self.resources_info, lambda_client) + self.layer_name = resources_info.get('lambda').get('supervisor').get('layer_name') + self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version') + self.layer = Layer(lambda_client) def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: return {'LayerName' : self.layer_name, diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index a0cb890a..940990d7 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -13,6 +13,7 @@ # limitations under the License. import json +from typing import Dict from enum import Enum from tabulate import tabulate import scar.logger as logger @@ -69,12 +70,10 @@ def _print_generic_response(response, output_type, aws_output, text_message=None logger.info_json(output) -def parse_lambda_function_creation_response(response, function, access_key): +def parse_lambda_function_creation_response(response, output_type, access_key): if response: - function_name = function.get('lambda', {}).get('name', '') - output_type = function.get('lambda', {}).get('cli_output', '') aws_output = 'LambdaOutput' - text_message = f"Function '{function_name}' successfully created." + text_message = f"Function '{response['FunctionName']}' successfully created." json_message = {aws_output : { 'AccessKey' : access_key, 'FunctionArn' : response['FunctionArn'], @@ -109,36 +108,32 @@ def parse_delete_api_response(response, api_id, output_type): _print_generic_response(response, output_type, 'APIGateway', text_message) -def parse_ls_response(lambda_functions, output_type): +def parse_ls_response(aws_resources: Dict, output_type: int) -> None: aws_output = 'Functions' result = [] text_message = "" if output_type == OutputType.VERBOSE.value: - result = lambda_functions + result = aws_resources else: - for lambdaf in lambda_functions: - result.append(_parse_lambda_function_info(lambdaf)) + for resources_info in aws_resources: + result.append(_parse_lambda_function_info(resources_info)) text_message = _get_table(result) json_message = { aws_output : result } _print_generic_response('', output_type, aws_output, text_message, json_output=json_message, verbose_output=json_message) -def _parse_lambda_function_info(function_info): - name = function_info.get('FunctionName', "-") - memory = function_info.get('MemorySize', "-") - timeout = function_info.get('Timeout', "-") - image_id = function_info['Environment']['Variables'].get('IMAGE_ID', "-") - api_gateway = function_info['Environment']['Variables'].get('API_GATEWAY_ID', "-") +def _parse_lambda_function_info(resources_info): + api_gateway = resources_info.get('lambda').get('environment').get('Variables').get('API_GATEWAY_ID', "-") if api_gateway != '-': - region = function_info['FunctionArn'].split(':')[3] - api_gateway = f"https://{api_gateway}.execute-api.{region}.amazonaws.com/scar/launch" - super_version = function_info.get('SupervisorVersion', '-') - return {'Name' : name, - 'Memory' : memory, - 'Timeout' : timeout, - 'Image_id': image_id, + stage_name = resources_info.get('api_gateway').get('stage_name') + region = resources_info.get('api_gateway').get('region') + api_gateway = f"https://{api_gateway}.execute-api.{region}.amazonaws.com/{stage_name}/launch" + return {'Name' : resources_info.get('lambda').get('name', "-"), + 'Memory' : resources_info.get('lambda').get('memory', "-"), + 'Timeout' : resources_info.get('lambda').get('timeout', "-"), + 'Image_id': resources_info.get('lambda').get('image', "-"), 'Api_gateway': api_gateway, - 'Sup_version': super_version} + 'Sup_version': resources_info.get('lambda').get('supervisor').get('version', '-')} def _get_table(functions_info): diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index a729ebfa..6be65f6d 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -20,7 +20,7 @@ from scar.utils import FileUtils -def _get_bucket_and_folders(storage_path: str) -> Tuple: +def get_bucket_and_folders(storage_path: str) -> Tuple: output_bucket = storage_path output_folders = "" output_path = storage_path.split("/", 1) @@ -35,7 +35,7 @@ class S3(GenericClient): def __init__(self, resources_info): super().__init__() - self.function_info = resources_info + self.resources_info = resources_info @excp.exception(logger) def create_bucket(self, bucket_name) -> None: @@ -43,15 +43,15 @@ def create_bucket(self, bucket_name) -> None: self.client.create_bucket(bucket_name) @excp.exception(logger) - def add_bucket_folder(self, folders: str) -> None: - self.upload_file(folder_name=folders) + def add_bucket_folder(self, bucket: str, folders: str) -> None: + self.upload_file(bucket, folder_name=folders) - def create_bucket_and_folders(self, storage_path: str) -> str: - bucket, folders = _get_bucket_and_folders(storage_path) + def create_bucket_and_folders(self, storage_path: str) -> Tuple: + bucket, folders = get_bucket_and_folders(storage_path) self.create_bucket(bucket) if folders: - self.add_bucket_folder(folders) - return bucket + self.add_bucket_folder(bucket, folders) + return bucket, folders def set_input_bucket_notification(self, bucket_name: str) -> None: # First check that the function doesn't have other configurations @@ -68,13 +68,13 @@ def delete_bucket_notification(self, bucket_name): bucket_conf = self.client.get_notification_configuration(bucket_name) if bucket_conf and "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] - filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.function_info.get('lambda').get('arn')] + filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.resources_info.get('lambda').get('arn')] notification = { "LambdaFunctionConfigurations": filter_conf } self.client.put_notification_configuration(bucket_name, notification) logger.info("Bucket notifications successfully deleted") def get_trigger_configuration(self, bucket_name: str) -> Dict: - return {"LambdaFunctionArn": self.function_info.get('lambda').get('arn'), + return {"LambdaFunctionArn": self.resources_info.get('lambda').get('arn'), "Events": [ "s3:ObjectCreated:*" ], "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": bucket_name }]}} } @@ -108,19 +108,23 @@ def upload_file(self, bucket: str, folder_name: str =None, file_path: str =None, @excp.exception(logger) def get_bucket_file_list(self): - bucket_name = self.function_info.s3.input_bucket - if self.client.find_bucket(bucket_name): - kwargs = {"Bucket" : bucket_name} - if hasattr(self.function_info.s3, "input_folder") and self.function_info.s3.input_folder: - kwargs["Prefix"] = self.function_info.s3.input_folder - return self.client.list_files(**kwargs) - else: - raise excp.BucketNotFoundError(bucket_name=bucket_name) + files = [] + for storage_info in self.resources_info.get('lambda').get('input'): + if storage_info.get('storage_provider') == 's3': + bucket_name, folder_path = get_bucket_and_folders(storage_info.get('path')) + if self.client.find_bucket(bucket_name): + kwargs = {"Bucket" : bucket_name} + if folder_path: + kwargs["Prefix"] = folder_path + files.extend(self.client.list_files(**kwargs)) + else: + raise excp.BucketNotFoundError(bucket_name=bucket_name) + return files def get_s3_event(self, s3_file_key): return {"Records": [{"eventSource": "aws:s3", - "s3" : {"bucket" : {"name": self.function_info.s3.input_bucket, - "arn": f'arn:aws:s3:::{self.function_info.s3.input_bucket}'}, + "s3" : {"bucket" : {"name": self.resources_info.s3.input_bucket, + "arn": f'arn:aws:s3:::{self.resources_info.s3.input_bucket}'}, "object" : {"key": s3_file_key}}}]} def get_s3_event_list(self, s3_file_keys): @@ -128,7 +132,7 @@ def get_s3_event_list(self, s3_file_keys): def download_file(self, bucket_name, file_key, file_path): kwargs = {'Bucket' : bucket_name, 'Key' : file_key} - logger.info("Downloading file '{0}' from bucket '{1}' in path '{2}'".format(file_key, bucket_name, file_path)) + logger.info(f"Downloading file '{file_key}' from bucket '{bucket_name}' in path '{file_path}'") with open(file_path, 'wb') as file: kwargs['Fileobj'] = file self.client.download_file(**kwargs) diff --git a/scar/scarcli.py b/scar/scarcli.py index 308b9e19..97efb23b 100755 --- a/scar/scarcli.py +++ b/scar/scarcli.py @@ -47,7 +47,7 @@ def parse_arguments(): # CMD >> SCAR.CONF merged_args = fdl.merge_conf(config_args, cmd_args) #self.cloud_provider.parse_arguments(merged_args) - FileUtils.create_config_file(merged_args) + FileUtils.create_tmp_config_file(merged_args) return func_call def main(): diff --git a/scar/utils.py b/scar/utils.py index 5656e5ec..e2cc295f 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -308,13 +308,13 @@ def write_yaml(file_path: str, content: Dict) -> None: yaml.safe_dump(content, cfg_file) @staticmethod - def create_config_file(cfg_args): + def create_tmp_config_file(cfg_args): cfg_path = FileUtils.join_paths(SysUtils.get_user_home_path(), ".scar", "scar_tmp.yaml") os.environ['SCAR_TMP_CFG'] = cfg_path FileUtils.write_yaml(cfg_path, cfg_args) @staticmethod - def load_config_file(): + def load_tmp_config_file(): return FileUtils.load_yaml(os.environ['SCAR_TMP_CFG']) @staticmethod From beb775f9994bda664e8f1f6436aaa84af426bc6a Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 25 Nov 2019 16:02:43 +0100 Subject: [PATCH 11/41] WIP - Add log functionality --- scar/providers/aws/batchfunction.py | 10 +++++----- scar/providers/aws/cloudwatchlogs.py | 2 +- scar/providers/aws/controller.py | 26 +++++++++++++++----------- scar/providers/aws/lambdafunction.py | 1 - 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index 5d446292..91da5ef5 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -209,10 +209,10 @@ def exist_compute_environments(self): response = self.client.describe_compute_environments(**creation_args) return len(response["computeEnvironments"]) > 0 - def describe_jobs(self, job_id): - describe_args = {'jobs': [job_id]} + def get_jobs_with_request_id(self) -> Dict: + describe_args = {'jobs': [self.resources_info.get('cloudwatch').get('request_id')]} return self.client.describe_jobs(**describe_args) - def exist_job(self, job_id): - response = self.describe_jobs(job_id) - return len(response["jobs"]) != 0 +# def exist_job(self, job_id: str) -> bool: +# response = self.describe_jobs(job_id) +# return len(response["jobs"]) != 0 diff --git a/scar/providers/aws/cloudwatchlogs.py b/scar/providers/aws/cloudwatchlogs.py index 8e08c398..934ec910 100644 --- a/scar/providers/aws/cloudwatchlogs.py +++ b/scar/providers/aws/cloudwatchlogs.py @@ -80,7 +80,7 @@ def delete_log_group(self, log_group_name: str) -> Dict: """Deletes a CloudWatch Log Group.""" return self.client.delete_log_group(log_group_name) - def getaws_log(self) -> str: + def get_aws_log(self) -> str: """Returns Lambda logs for an specific lambda function.""" function_logs = "" try: diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 15954eed..79b75cfd 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -147,7 +147,7 @@ def init(self) -> None: self._create_lambda_function(resources_info) self._create_log_group(resources_info) self._create_s3_buckets(resources_info) - # The api_gateway permissions are added after the function is created + # The api_gateway permissions must be added after the function is created self._add_api_gateway_permissions(resources_info) self._create_batch_environment(resources_info) # self._preheat_function() @@ -172,6 +172,7 @@ def run(self): response = Lambda(resources_info).launch_lambda_instance() if self.scar_info.get("output_file", False): response['OutputFile'] = self.scar_info.get("output_file") + response['OutputType'] = self.scar_info.get("cli_output") response_parser.parse_invocation_response(**response) 'TODO FINISH' # if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): @@ -218,11 +219,15 @@ def rm(self): @excp.exception(logger) def log(self): - 'TODO' -# aws_log = self.cloudwatch_logs.get_aws_log() -# batch_logs = self._get_batch_logs() -# aws_log += batch_logs if batch_logs else "" -# print(aws_log) + index = 0 + if len(self.aws_resources) > 1: + index = _choose_function(self.aws_resources) + # We only return the logs of one function each time + if index >= 0: + aws_log = CloudWatchLogs(self.aws_resources[index]).get_aws_log() + batch_logs = self._get_batch_logs(self.aws_resources[index]) + aws_log += batch_logs if batch_logs else "" + print(aws_log) @excp.exception(logger) def put(self): @@ -355,12 +360,11 @@ def _get_all_functions(self): arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id()) return Lambda(resources_info).get_all_functions(arn_list) - def _get_batch_logs(self) -> str: + def _get_batch_logs(self, resources_info: Dict) -> str: logs = "" -# if hasattr(self.aws_properties.cloudwatch, "request_id") and \ -# self.batch.exist_job(self.aws_properties.cloudwatch.request_id): -# batch_jobs = self.batch.describe_jobs(self.aws_properties.cloudwatch.request_id) -# logs = self.cloudwatch_logs.get_batch_job_log(batch_jobs["jobs"]) + if resources_info.get('cloudwatch').get('request_id', False): + batch_jobs = Batch(resources_info).get_jobs_with_request_id() + logs = CloudWatchLogs(resources_info).get_batch_job_log(batch_jobs["jobs"]) return logs # def _preheat_function(self): diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index b8510df6..a4a6f413 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -142,7 +142,6 @@ def launch_lambda_instance(self): response = self._invoke_lambda_function() response_args = {'Response' : response, 'FunctionName' : self.function.get('name'), - 'OutputType' : self.function.get('cli_output'), 'IsAsynchronous' : self.function.get('asynchronous')} return response_args From 30187624db0038e750c8635c3b6f5df84dd0c63b Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 25 Nov 2019 16:36:27 +0100 Subject: [PATCH 12/41] WIP - Add invoke functionality --- scar/parser/cfgfile.py | 2 +- scar/providers/aws/controller.py | 43 +++++++++++++++---------- scar/providers/aws/lambdafunction.py | 48 ++++++++++++++-------------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index ef85256d..d7684448 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -64,7 +64,7 @@ "api_gateway": { "boto_profile": "default", "region": "us-east-1", - "endpoint": "https://{api_id}.execute-api.{api_region}.amazonaws.com/scar/launch", + "endpoint": "https://{api_id}.execute-api.{api_region}.amazonaws.com/{stage_name}/launch", 'request_parameters': {"integration.request.header.X-Amz-Invocation-Type": "method.request.header.X-Amz-Invocation-Type"}, # ANY, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 79b75cfd..fbf88aa8 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -154,27 +154,31 @@ def init(self) -> None: @excp.exception(logger) def invoke(self): - 'TODO' -# self._update_local_function_properties() -# response = self.aws_lambda.call_http_endpoint() -# response_parser.parse_http_response(response, -# self.aws_properties.lambdaf.name, -# self.aws_properties.lambdaf.asynchronous, -# self.aws_properties.output, -# getattr(self.scar, "output_file", "")) + index = 0 + if len(self.aws_resources) > 1: + index = _choose_function(self.aws_resources) + if index >= 0: + resources_info = self.aws_resources[index] + response = Lambda(resources_info).call_http_endpoint() + response_parser.parse_http_response(response, + resources_info.get('lambda').get('name'), + resources_info.get('lambda').get('asynchronous'), + self.scar_info.get('cli_output'), + self.scar_info.get('output_file', '')) @excp.exception(logger) def run(self): index = 0 if len(self.aws_resources) > 1: index = _choose_function(self.aws_resources) - resources_info = self.aws_resources[index] - response = Lambda(resources_info).launch_lambda_instance() - if self.scar_info.get("output_file", False): - response['OutputFile'] = self.scar_info.get("output_file") - response['OutputType'] = self.scar_info.get("cli_output") - response_parser.parse_invocation_response(**response) - 'TODO FINISH' + if index >= 0: + resources_info = self.aws_resources[index] + response = Lambda(resources_info).launch_lambda_instance() + if self.scar_info.get("output_file", False): + response['OutputFile'] = self.scar_info.get("output_file") + response['OutputType'] = self.scar_info.get("cli_output") + response_parser.parse_invocation_response(**response) + 'TODO FINISH' # if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): # self._process_input_bucket_calls() @@ -388,8 +392,13 @@ def _update_all_functions(self, lambda_functions): for function_info in lambda_functions: self.aws_lambda.update_function_configuration(function_info) - def _update_local_function_properties(self, function_info): - self._reset_aws_properties() +# def _set_api_id(self, resources_info: Dict) -> None: +# api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') +# if api_gateway_id: +# resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id + +# def _update_local_function_properties(self, function_info): +# self._reset_aws_properties() # """Update the defined properties with the AWS information.""" # if function_info: # self.aws_properties.lambdaf.update_properties(**function_info) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index a4a6f413..cdbd3fff 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -14,6 +14,7 @@ import base64 import json +from scar.http.request import call_http_endpoint from multiprocessing.pool import ThreadPool from botocore.exceptions import ClientError from scar.providers.aws import GenericClient @@ -278,36 +279,35 @@ def get_api_gateway_id(self): return env_vars['Variables'].get('API_GATEWAY_ID', '') def _get_api_gateway_url(self): - 'TODO' -# api_id = self.get_api_gateway_id() -# if not api_id: -# raise excp.ApiEndpointNotFoundError(self.function.get('name')) -# return f'https://{api_id}.execute-api.{self.resources_info.region}.amazonaws.com/scar/launch' + api_id = self.get_api_gateway_id() + if not api_id: + raise excp.ApiEndpointNotFoundError(self.function.get('name')) + return self.resources_info.get('api_gateway').get('endpoint').format(api_id=api_id, + api_region=self.resources_info.get('api_gateway').get('region'), + stage_name=self.resources_info.get('api_gateway').get('stage_name')) + def call_http_endpoint(self): - 'TODO' -# invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} -# if hasattr(self.aws, "api_gateway"): -# self._set_invoke_args(invoke_args) -# return request.call_http_endpoint(self._get_api_gateway_url(), **invoke_args) + invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} + self._set_invoke_args(invoke_args) + return call_http_endpoint(self._get_api_gateway_url(), **invoke_args) def _set_invoke_args(self, invoke_args): - 'TODO' -# if hasattr(self.aws.api_gateway, "data_binary"): -# invoke_args['data'] = self._get_b64encoded_binary_data(self.aws.api_gateway.data_binary) -# invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} -# if hasattr(self.aws.api_gateway, "parameters"): -# invoke_args['params'] = self._parse_http_parameters(self.aws.api_gateway.parameters) -# if hasattr(self.aws.api_gateway, "json_data"): -# invoke_args['data'] = self._parse_http_parameters(self.aws.api_gateway.json_data) -# invoke_args['headers'] = {'Content-Type': 'application/json'} + if self.resources_info.get('api_gateway').get('data_binary', False): + invoke_args['data'] = self._get_b64encoded_binary_data() + invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} + if self.resources_info.get('api_gateway').get('parameters', False): + invoke_args['params'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('parameters')) + if self.resources_info.get('api_gateway').get('json_data', False): + invoke_args['data'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('json_data')) + invoke_args['headers'] = {'Content-Type': 'application/json'} def _parse_http_parameters(self, parameters): return parameters if type(parameters) is dict else json.loads(parameters) @excp.exception(logger) - def _get_b64encoded_binary_data(self, data_path): - if data_path: - AWSValidator.validate_http_payload_size(data_path, self.is_asynchronous()) - with open(data_path, 'rb') as data_file: - return base64.b64encode(data_file.read()) + def _get_b64encoded_binary_data(self): + data_path = self.resources_info.get('api_gateway').get('data_binary') + AWSValidator.validate_http_payload_size(data_path, self.is_asynchronous()) + with open(data_path, 'rb') as data_file: + return base64.b64encode(data_file.read()) From 13fb2f78453f9bcb7ed19ddf0f90ab3e40018a5c Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 25 Nov 2019 18:30:12 +0100 Subject: [PATCH 13/41] WIP - Add launch lambda function from s3 bucket files --- scar/parser/cfgfile.py | 20 +++++++++- scar/parser/cli/subparsers.py | 1 + scar/providers/aws/controller.py | 57 +++++++++++++++------------- scar/providers/aws/lambdafunction.py | 10 ++--- scar/providers/aws/s3.py | 47 +++++++++++++---------- 5 files changed, 82 insertions(+), 53 deletions(-) diff --git a/scar/parser/cfgfile.py b/scar/parser/cfgfile.py index d7684448..99cffbc0 100644 --- a/scar/parser/cfgfile.py +++ b/scar/parser/cfgfile.py @@ -21,7 +21,7 @@ _DEFAULT_CFG = { "scar": { - "config_version": "1.0.8" + "config_version": "1.0.9" }, "aws": { "iam": {"boto_profile": "default", @@ -61,6 +61,24 @@ 'license_info' : 'Apache 2.0' } }, + "s3": { + "boto_profile": "default", + "region": "us-east-1", + "event" : { + "Records": [{ + "eventSource": "aws:s3", + "s3" : { + "bucket" : { + "name": "{bucket_name}", + "arn": "arn:aws:s3:::{bucket_name}" + }, + "object" : { + "key": "{file_key}" + } + } + }] + } + }, "api_gateway": { "boto_profile": "default", "region": "us-east-1", diff --git a/scar/parser/cli/subparsers.py b/scar/parser/cli/subparsers.py index 867da616..58ac64cb 100644 --- a/scar/parser/cli/subparsers.py +++ b/scar/parser/cli/subparsers.py @@ -110,6 +110,7 @@ def _add_run_parser(self): group.add_argument("-n", "--name", help="Lambda function name") group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") run.add_argument("-s", "--run-script", help="Path to the script passed to the function") + run.add_argument("-ib", "--input-bucket", help=("Bucket name with files to launch the function.")) run.add_argument('c_args', nargs=argparse.REMAINDER, help="Arguments passed to the container.") diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index fbf88aa8..d8004266 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -171,16 +171,19 @@ def run(self): index = 0 if len(self.aws_resources) > 1: index = _choose_function(self.aws_resources) - if index >= 0: + if index >= 0: resources_info = self.aws_resources[index] - response = Lambda(resources_info).launch_lambda_instance() - if self.scar_info.get("output_file", False): - response['OutputFile'] = self.scar_info.get("output_file") - response['OutputType'] = self.scar_info.get("cli_output") - response_parser.parse_invocation_response(**response) - 'TODO FINISH' -# if hasattr(self.aws_properties, "s3") and hasattr(self.aws_properties.s3, "input_bucket"): -# self._process_input_bucket_calls() + using_s3_bucket = False + if resources_info.get('lambda').get('input', False): + for storage in resources_info.get('lambda').get('input'): + if storage.get('storage_provider') == 's3': + print("This function has an associated 'S3' input bucket.") + response = input(f"Do you want to launch the function using the files in '{storage.get('path')}'? [Y/n] ") + if response in ('Y', 'y'): + using_s3_bucket = True + self._process_s3_input_bucket_calls(resources_info, storage) + if not using_s3_bucket: + self._launch_lambda_function(resources_info) @excp.exception(logger) def update(self): @@ -197,7 +200,7 @@ def ls(self): if resources_info.get('lambda').get('input', False): file_list = S3(resources_info).get_bucket_file_list() for file_info in file_list: - print(file_info) + logger.info(file_info) else: aws_resources = self._get_all_functions() response_parser.parse_ls_response(aws_resources, self.scar_info.get('cli_output')) @@ -231,7 +234,7 @@ def log(self): aws_log = CloudWatchLogs(self.aws_resources[index]).get_aws_log() batch_logs = self._get_batch_logs(self.aws_resources[index]) aws_log += batch_logs if batch_logs else "" - print(aws_log) + logger.info(aws_log) @excp.exception(logger) def put(self): @@ -249,7 +252,6 @@ def get(self): def _create_api_gateway(self, resources_info: Dict): if resources_info.get("api_gateway", {}).get('name', False): APIGateway(resources_info).create_api_gateway() - @excp.exception(logger) def _create_lambda_function(self, resources_info: Dict) -> None: @@ -259,7 +261,6 @@ def _create_lambda_function(self, resources_info: Dict) -> None: self.scar_info.get('cli_output'), lambda_client.get_access_key()) - @excp.exception(logger) def _create_log_group(self, resources_info: Dict) -> None: cloudwatch_logs = CloudWatchLogs(resources_info) @@ -267,7 +268,6 @@ def _create_log_group(self, resources_info: Dict) -> None: response_parser.parse_log_group_creation_response(response, cloudwatch_logs.get_log_group_name(), self.scar_info.get('cli_output')) - @excp.exception(logger) def _create_s3_buckets(self, resources_info: Dict) -> None: @@ -285,13 +285,11 @@ def _create_s3_buckets(self, resources_info: Dict) -> None: if bucket.get('storage_provider') == 's3': s3_service.create_bucket_and_folders(bucket.get('path')) - @excp.exception(logger) def _add_api_gateway_permissions(self, resources_info: Dict): if resources_info.get("api_gateway").get('name', False): Lambda(resources_info).add_invocation_permission_from_api_gateway() - @excp.exception(logger) def _create_batch_environment(self, resources_info: Dict) -> None: mode = resources_info.get('lambda').get('execution_mode') @@ -314,7 +312,6 @@ def _delete_resources(self, resources_info: Dict) -> None: # Delete resources batch self._delete_batch_resources(resources_info) - def _delete_api_gateway(self, resources_info: Dict) -> None: api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') if api_gateway_id: @@ -324,7 +321,6 @@ def _delete_api_gateway(self, resources_info: Dict) -> None: api_gateway_id, self.scar_info.get('cli_output')) - def _delete_logs(self, resources_info: Dict): cloudwatch_logs = CloudWatchLogs(resources_info) log_group_name = cloudwatch_logs.get_log_group_name(resources_info.get('lambda').get('name')) @@ -333,7 +329,6 @@ def _delete_logs(self, resources_info: Dict): log_group_name, self.scar_info.get('cli_output')) - def _delete_bucket_notifications(self, resources_info: Dict) -> None: if resources_info.get('lambda').get('input', False): for input_storage in resources_info.get('lambda').get('input'): @@ -341,14 +336,12 @@ def _delete_bucket_notifications(self, resources_info: Dict) -> None: bucket_name = input_storage.get('path').split("/", 1)[0] S3(resources_info).delete_bucket_notification(bucket_name) - def _delete_lambda_function(self, resources_info: Dict) -> None: response = Lambda(resources_info).delete_function() response_parser.parse_delete_function_response(response, resources_info.get('lambda').get('name'), self.scar_info.get('cli_output')) - def _delete_batch_resources(self, resources_info: Dict) -> None: batch = Batch(resources_info) if batch.exist_compute_environments(): @@ -358,6 +351,13 @@ def _delete_batch_resources(self, resources_info: Dict) -> None: ### ------------ ### ############################################ + def _launch_lambda_function(self, resources_info): + response = Lambda(resources_info).launch_lambda_instance() + if self.scar_info.get("output_file", False): + response['OutputFile'] = self.scar_info.get("output_file") + response['OutputType'] = self.scar_info.get("cli_output") + response_parser.parse_invocation_response(**response) + def _get_all_functions(self): # Return the resources of the region in the scar's configuration file resources_info = self.aws_resources[0] @@ -376,17 +376,20 @@ def _get_batch_logs(self, resources_info: Dict) -> str: # if hasattr(self.scar, "preheat"): # self.aws_lambda.preheat_function() - def _process_input_bucket_calls(self): - s3_file_list = self.aws_s3.get_bucket_file_list() + def _process_s3_input_bucket_calls(self, resources_info: Dict, storage: Dict) -> None: + s3_service = S3(resources_info) + lambda_service = Lambda(resources_info) + s3_file_list = s3_service.get_bucket_file_list(storage) + bucket_name, _ = get_bucket_and_folders(storage.get('path')) logger.info(f"Files found: '{s3_file_list}'") # First do a request response invocation to prepare the lambda environment if s3_file_list: - s3_event = self.aws_s3.get_s3_event(s3_file_list.pop(0)) - self.aws_lambda.launch_request_response_event(s3_event) + s3_event = s3_service.get_s3_event(bucket_name, s3_file_list.pop(0)) + lambda_service.launch_request_response_event(s3_event) # If the list has more elements, invoke functions asynchronously if s3_file_list: - s3_event_list = self.aws_s3.get_s3_event_list(s3_file_list) - self.aws_lambda.process_asynchronous_lambda_invocations(s3_event_list) + s3_event_list = s3_service.get_s3_event_list(bucket_name, s3_file_list) + lambda_service.process_asynchronous_lambda_invocations(s3_event_list) def _update_all_functions(self, lambda_functions): for function_info in lambda_functions: diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index cdbd3fff..e104c4c2 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -119,15 +119,13 @@ def launch_request_response_event(self, s3_event): return self._launch_s3_event(s3_event) def _launch_s3_event(self, s3_event): - 'TODO' -# self.aws.lambdaf.payload = s3_event -# logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") -# return self.launch_lambda_instance() + self.function['payload'] = s3_event + logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'") + return self.launch_lambda_instance() def process_asynchronous_lambda_invocations(self, s3_event_list): if (len(s3_event_list) > MAX_CONCURRENT_INVOCATIONS): - s3_file_chunk_list = DataTypesUtils.divide_list_in_chunks(s3_event_list, MAX_CONCURRENT_INVOCATIONS) - for s3_file_chunk in s3_file_chunk_list: + for s3_file_chunk in DataTypesUtils.divide_list_in_chunks(s3_event_list, MAX_CONCURRENT_INVOCATIONS): self._launch_concurrent_lambda_invocations(s3_file_chunk) else: self._launch_concurrent_lambda_invocations(s3_event_list) diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index 6be65f6d..acfb2037 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -13,7 +13,7 @@ # limitations under the License. import os -from typing import Tuple, Dict +from typing import Tuple, Dict, List from scar.providers.aws import GenericClient import scar.exceptions as excp import scar.logger as logger @@ -107,28 +107,37 @@ def upload_file(self, bucket: str, folder_name: str =None, file_path: str =None, self.client.upload_file(**kwargs) @excp.exception(logger) - def get_bucket_file_list(self): + def get_bucket_file_list(self, storage: Dict = None): files = [] - for storage_info in self.resources_info.get('lambda').get('input'): - if storage_info.get('storage_provider') == 's3': - bucket_name, folder_path = get_bucket_and_folders(storage_info.get('path')) - if self.client.find_bucket(bucket_name): - kwargs = {"Bucket" : bucket_name} - if folder_path: - kwargs["Prefix"] = folder_path - files.extend(self.client.list_files(**kwargs)) - else: - raise excp.BucketNotFoundError(bucket_name=bucket_name) + if storage: + files = self._list_storage_files(storage) + else: + for storage_info in self.resources_info.get('lambda').get('input'): + if storage_info.get('storage_provider') == 's3': + files.extend(self._list_storage_files(storage_info)) + return files + + def _list_storage_files(self, storage: Dict) -> List: + files = [] + bucket_name, folder_path = get_bucket_and_folders(storage.get('path')) + if self.client.find_bucket(bucket_name): + kwargs = {"Bucket" : bucket_name} + if folder_path: + kwargs["Prefix"] = folder_path + files = (self.client.list_files(**kwargs)) + else: + raise excp.BucketNotFoundError(bucket_name=bucket_name) return files - def get_s3_event(self, s3_file_key): - return {"Records": [{"eventSource": "aws:s3", - "s3" : {"bucket" : {"name": self.resources_info.s3.input_bucket, - "arn": f'arn:aws:s3:::{self.resources_info.s3.input_bucket}'}, - "object" : {"key": s3_file_key}}}]} + def get_s3_event(self, bucket_name, file_key): + event = self.resources_info.get("s3").get("event") + event['Records'][0]['s3']['bucket']['name'] = bucket_name + event['Records'][0]['s3']['bucket']['arn'] = event['Records'][0]['s3']['bucket']['arn'].format(bucket_name=bucket_name) + event['Records'][0]['s3']['object']['key'] = file_key + return event - def get_s3_event_list(self, s3_file_keys): - return [self.get_s3_event(s3_key) for s3_key in s3_file_keys] + def get_s3_event_list(self, bucket_name, file_keys): + return [self.get_s3_event(bucket_name, file_key) for file_key in file_keys] def download_file(self, bucket_name, file_key, file_path): kwargs = {'Bucket' : bucket_name, 'Key' : file_key} From 8bc41a8c23d5d3e8268e339561f44d148ff91973 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 26 Nov 2019 11:37:00 +0100 Subject: [PATCH 14/41] Fix error getting the api endpoint --- scar/providers/aws/apigateway.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scar/providers/aws/apigateway.py b/scar/providers/aws/apigateway.py index b9f25d26..23910fd9 100644 --- a/scar/providers/aws/apigateway.py +++ b/scar/providers/aws/apigateway.py @@ -66,7 +66,9 @@ def _set_resource_info_id(self, resource_info: Dict) -> None: self.api['resource_id'] = resource_info.get('id', '') def _get_endpoint(self) -> str: - endpoint_args = {'api_id': self.api.get('id', ''), 'api_region': self.api.get('region', '')} + endpoint_args = {'api_id': self.api.get('id', ''), + 'api_region': self.api.get('region', ''), + 'stage_name': self.api.get('stage_name', '')} return self.api.get('endpoint', '').format(**endpoint_args) def create_api_gateway(self) -> None: From 79eafa52c6cc70267e9f302ef8c2d74ea4c1c037 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 26 Nov 2019 15:12:56 +0100 Subject: [PATCH 15/41] WIP - Fix error decoding the output of the invoke command --- scar/providers/aws/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index 940990d7..cbcac8d4 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -40,7 +40,7 @@ def parse_http_response(response, function_name, asynch, output_type, output_fil else: text_message += f"\nLog Group Name: {response.headers['amz-log-group-name']}\n" text_message += f"Log Stream Name: {response.headers['amz-log-stream-name']}\n" - text_message += json.loads(response.text)["udocker_output"] + text_message += StrUtils.base64_to_utf8_string(response.text) else: if asynch and response.status_code == 502: text_message = f"Function '{function_name}' launched sucessfully." From 196cf864c013078f869fa6d68b0d1bafe6158e05 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 26 Nov 2019 17:27:41 +0100 Subject: [PATCH 16/41] WIP - Multiple fixes / Remove update cmd --- scar/cmdtemplate.py | 5 - scar/parser/cli/__init__.py | 6 +- scar/parser/cli/subparsers.py | 29 ++-- scar/providers/aws/clients/lambdafunction.py | 2 +- scar/providers/aws/cloudwatchlogs.py | 46 +++--- scar/providers/aws/controller.py | 147 ++++++------------- scar/providers/aws/lambdafunction.py | 60 +------- scar/providers/aws/response.py | 14 +- scar/providers/aws/s3.py | 2 +- scar/providers/aws/validators.py | 9 +- scar/utils.py | 15 +- scar/validator.py | 36 ----- 12 files changed, 124 insertions(+), 247 deletions(-) delete mode 100644 scar/validator.py diff --git a/scar/cmdtemplate.py b/scar/cmdtemplate.py index a2d80a24..a6009c2a 100644 --- a/scar/cmdtemplate.py +++ b/scar/cmdtemplate.py @@ -19,7 +19,6 @@ class CallType(Enum): INIT = "init" INVOKE = "invoke" RUN = "run" - UPDATE = "update" LS = "ls" RM = "rm" LOG = "log" @@ -42,10 +41,6 @@ def invoke(self): def run(self): pass - @abc.abstractmethod - def update(self): - pass - @abc.abstractmethod def ls(self): pass diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index b6b38177..714dd398 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -48,9 +48,7 @@ def _set_args(args: Dict, key: str, val: str) -> None: def _parse_scar_args(cmd_args: Dict) -> Dict: - scar_args = ['conf_file', 'json', - 'verbose', 'path', - 'preheat', 'execution_mode', + scar_args = ['conf_file', 'json', 'verbose', 'path', 'execution_mode', 'output_file', 'supervisor_version', 'all'] return {'scar' : DataTypesUtils.parse_arg_list(scar_args, cmd_args)} @@ -64,7 +62,7 @@ def _parse_lambda_args(cmd_args: Dict) -> Dict: lambda_arg_list = ['name', 'asynchronous', 'init_script', 'run_script', 'c_args', 'memory', 'timeout', 'timeout_threshold', 'image', 'image_file', 'description', 'lambda_role', 'extra_payload', ('environment', 'environment_variables'), - 'layers', 'lambda_environment', 'list_layers', 'log_level'] + 'layers', 'lambda_environment', 'list_layers', 'log_level', 'preheat'] lambda_args = DataTypesUtils.parse_arg_list(lambda_arg_list, cmd_args) # Standardize log level if defined if "log_level" in lambda_args: diff --git a/scar/parser/cli/subparsers.py b/scar/parser/cli/subparsers.py index 58ac64cb..3578034f 100644 --- a/scar/parser/cli/subparsers.py +++ b/scar/parser/cli/subparsers.py @@ -19,7 +19,7 @@ EXEC = "exec_parser" STORAGE = "storage_parser" -INIT_UPDATE_PARENTS = [PROFILE, FUNCTION_DEFINITION, OUTPUT] +INIT_PARENTS = [PROFILE, FUNCTION_DEFINITION, OUTPUT] INVOKE_PARENTS = [PROFILE, EXEC] RUN_PARENTS = [PROFILE, EXEC, OUTPUT] RM_LS_PARENTS = [PROFILE, OUTPUT] @@ -41,7 +41,7 @@ def add_subparser(self, name): def _add_init_parser(self): init = self.subparser.add_parser('init', - parents=self._get_parents(INIT_UPDATE_PARENTS), + parents=self._get_parents(INIT_PARENTS), help="Create lambda function") # Set default function init.set_defaults(func="init") @@ -74,7 +74,7 @@ def _add_invoke_parser(self): parents=self._get_parents(INVOKE_PARENTS), help="Call a lambda function using an HTTP request") # Set default function - invoke.set_defaults(func='invoke') + invoke.set_defaults(func='invoke') group = invoke.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -89,28 +89,17 @@ def _add_invoke_parser(self): "you can pass the parameters here (i.e. '{\"key1\": " "\"value1\", \"key2\": [\"value2\", \"value3\"]}').")) - def _add_update_parser(self): - update = self.subparser.add_parser('update', - parents=self._get_parents(INIT_UPDATE_PARENTS), - help="Update function properties") - # Set default function - update.set_defaults(func='update') - group = update.add_mutually_exclusive_group(required=True) - group.add_argument("-n", "--name", help="Lambda function name") - group.add_argument("-a", "--all", help="Update all lambda functions", action="store_true") - group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") - def _add_run_parser(self): run = self.subparser.add_parser('run', parents=self._get_parents(RUN_PARENTS), help="Deploy function") # Set default function - run.set_defaults(func='run') + run.set_defaults(func='run') group = run.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") group.add_argument("-f", "--conf-file", help="Yaml file with the function configuration") run.add_argument("-s", "--run-script", help="Path to the script passed to the function") - run.add_argument("-ib", "--input-bucket", help=("Bucket name with files to launch the function.")) + run.add_argument("-ib", "--input-bucket", help=("Bucket name with files to launch the function.")) run.add_argument('c_args', nargs=argparse.REMAINDER, help="Arguments passed to the container.") @@ -120,7 +109,7 @@ def _add_rm_parser(self): parents=self._get_parents(RM_LS_PARENTS), help="Delete function") # Set default function - rm.set_defaults(func='rm') + rm.set_defaults(func='rm') group = rm.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -135,7 +124,7 @@ def _add_log_parser(self): parents=self._get_parents(LOG_PARENTS), help="Show the logs for the lambda function") # Set default function - log.set_defaults(func='log') + log.set_defaults(func='log') group = log.add_mutually_exclusive_group(required=True) group.add_argument("-n", "--name", help="Lambda function name") @@ -161,7 +150,7 @@ def _add_put_parser(self): parents=self._get_parents(PUT_GET_PARENTS), help="Upload file(s) to bucket") # Set default function - put.set_defaults(func='put') + put.set_defaults(func='put') def _add_get_parser(self): get = self.subparser.add_parser('get', @@ -169,4 +158,4 @@ def _add_get_parser(self): help="Download file(s) from bucket") # Set default function get.set_defaults(func='get') - + diff --git a/scar/providers/aws/clients/lambdafunction.py b/scar/providers/aws/clients/lambdafunction.py index d87dd3c3..368a5fd0 100644 --- a/scar/providers/aws/clients/lambdafunction.py +++ b/scar/providers/aws/clients/lambdafunction.py @@ -34,7 +34,7 @@ def create_function(self, **kwargs: Dict) -> Dict: logger.debug("Creating lambda function.") return self.client.create_function(**kwargs) - def get_function_info(self, function_name_or_arn: str) -> Dict: + def get_function_configuration(self, function_name_or_arn: str) -> Dict: """Returns the configuration information of the Lambda function.""" function_info = self.client.get_function_configuration(FunctionName=function_name_or_arn) diff --git a/scar/providers/aws/cloudwatchlogs.py b/scar/providers/aws/cloudwatchlogs.py index 934ec910..10db574f 100644 --- a/scar/providers/aws/cloudwatchlogs.py +++ b/scar/providers/aws/cloudwatchlogs.py @@ -17,6 +17,7 @@ from typing import List, Dict from botocore.exceptions import ClientError from scar.providers.aws import GenericClient +from scar.providers.aws.batchfunction import Batch import scar.logger as logger @@ -65,22 +66,7 @@ def _parse_logs_with_requestid(self, function_logs: str) -> str: parsed_msg += f'{line}\n' return parsed_msg - def create_log_group(self) -> Dict: - """Creates a CloudWatch Log Group.""" - creation_args = self._get_log_group_name_arg() - creation_args['tags'] = self.resources_info.get('lambda').get('tags') - response = self.client.create_log_group(**creation_args) - # Set retention policy into the log group - retention_args = self._get_log_group_name_arg() - retention_args['retentionInDays'] = self.cloudwatch.get('log_retention_policy_in_days') - self.client.set_log_retention_policy(**retention_args) - return response - - def delete_log_group(self, log_group_name: str) -> Dict: - """Deletes a CloudWatch Log Group.""" - return self.client.delete_log_group(log_group_name) - - def get_aws_log(self) -> str: + def _get_lambda_logs(self): """Returns Lambda logs for an specific lambda function.""" function_logs = "" try: @@ -93,8 +79,8 @@ def get_aws_log(self) -> str: except ClientError as cerr: logger.warning("Error getting the function logs: %s" % cerr) return function_logs - - def get_batch_job_log(self, jobs_info: List) -> str: + + def _get_batch_job_log(self, jobs_info: List) -> str: """Returns Batch logs for an specific job.""" batch_logs = "" if jobs_info: @@ -109,3 +95,27 @@ def get_batch_job_log(self, jobs_info: List) -> str: for event in response.get("events", {})] batch_logs += '\n'.join(msgs) return batch_logs + + def create_log_group(self) -> Dict: + """Creates a CloudWatch Log Group.""" + creation_args = self._get_log_group_name_arg() + creation_args['tags'] = self.resources_info.get('lambda').get('tags') + response = self.client.create_log_group(**creation_args) + # Set retention policy into the log group + retention_args = self._get_log_group_name_arg() + retention_args['retentionInDays'] = self.cloudwatch.get('log_retention_policy_in_days') + self.client.set_log_retention_policy(**retention_args) + return response + + def delete_log_group(self, log_group_name: str) -> Dict: + """Deletes a CloudWatch Log Group.""" + return self.client.delete_log_group(log_group_name) + + def get_aws_logs(self) -> str: + """Returns Cloudwatch logs for an specific lambda function and batch job (if any).""" + aws_logs = self._get_lambda_logs() + batch_logs = "" + if self.resources_info.get('cloudwatch').get('request_id', False): + batch_jobs = Batch(self.resources_info).get_jobs_with_request_id() + batch_logs = self._get_batch_job_log(batch_jobs["jobs"]) + return aws_logs + batch_logs if batch_logs else aws_logs diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index d8004266..a1047d49 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -58,6 +58,15 @@ def _choose_function(aws_resources: Dict) -> int: return int(i) - 1 return None + +def _get_all_functions(resources_info: Dict): + arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id()) + return Lambda(resources_info).get_all_functions(arn_list) + +def _check_preheat_function(resources_info: Dict): + if resources_info.get('lambda').get('preheat', False): + Lambda(resources_info).preheat_function() + ############################################ ### ADD EXTRA PROPERTIES ### ############################################ @@ -107,7 +116,7 @@ def _add_config_file_path(scar_info: Dict, resources_info: Dict): resources_info['lambda']['image_file']) if resources_info['lambda'].get('run_script', False): resources_info['lambda']['run_script'] = FileUtils.join_paths(resources_info['lambda']['config_path'], - resources_info['lambda']['run_script']) + resources_info['lambda']['run_script']) ############################################ ### AWS CONTROLLER ### @@ -120,7 +129,7 @@ class AWS(Commands): def __init__(self, func_call): self.raw_args = FileUtils.load_tmp_config_file() - self.validate_arguments(self.raw_args) + AWSValidator.validate_kwargs(self.raw_args) self.aws_resources = self.raw_args.get('functions', {}).get('aws', {}) self.storages = self.raw_args.get('storages', {}) self.scar_info = self.raw_args.get('scar', {}) @@ -128,18 +137,12 @@ def __init__(self, func_call): # Call the user's command getattr(self, func_call)() - @AWSValidator.validate() - @excp.exception(logger) - def validate_arguments(self, merged_args: Dict) -> None: - pass - ############################################ ### AWS COMMANDS ### ############################################ @excp.exception(logger) def init(self) -> None: - # supervisor_version = self.scar_info.get('supervisor_version', 'latest') for resources_info in self.aws_resources: _check_function_defined(resources_info) # We have to create the gateway before creating the function @@ -150,7 +153,7 @@ def init(self) -> None: # The api_gateway permissions must be added after the function is created self._add_api_gateway_permissions(resources_info) self._create_batch_environment(resources_info) - # self._preheat_function() + _check_preheat_function(resources_info) @excp.exception(logger) def invoke(self): @@ -171,7 +174,7 @@ def run(self): index = 0 if len(self.aws_resources) > 1: index = _choose_function(self.aws_resources) - if index >= 0: + if index >= 0: resources_info = self.aws_resources[index] using_s3_bucket = False if resources_info.get('lambda').get('input', False): @@ -183,15 +186,11 @@ def run(self): using_s3_bucket = True self._process_s3_input_bucket_calls(resources_info, storage) if not using_s3_bucket: - self._launch_lambda_function(resources_info) - - @excp.exception(logger) - def update(self): - 'TODO' -# if hasattr(self.aws_properties.lambdaf, "all") and self.aws_properties.lambdaf.all: -# self._update_all_functions(self._get_all_functions()) -# else: -# self.aws_lambda.update_function_configuration() + response = Lambda(resources_info).launch_lambda_instance() + if self.scar_info.get("output_file", False): + response['OutputFile'] = self.scar_info.get("output_file") + response['OutputType'] = self.scar_info.get("cli_output") + response_parser.parse_invocation_response(**response) @excp.exception(logger) def ls(self): @@ -200,15 +199,17 @@ def ls(self): if resources_info.get('lambda').get('input', False): file_list = S3(resources_info).get_bucket_file_list() for file_info in file_list: - logger.info(file_info) + logger.info(file_info) else: - aws_resources = self._get_all_functions() - response_parser.parse_ls_response(aws_resources, self.scar_info.get('cli_output')) + # Return the resources of the region in the scar's configuration file + aws_resources = _get_all_functions(self.aws_resources[0]) + response_parser.parse_ls_response(aws_resources, self.scar_info.get('cli_output')) @excp.exception(logger) def rm(self): if self.scar_info.get('all', False): - for resources_info in self._get_all_functions(): + # Return the resources of the region in the scar's configuration file + for resources_info in _get_all_functions(self.aws_resources[0]): self._delete_resources(resources_info) else: index = 0 @@ -219,10 +220,10 @@ def rm(self): for resources_info in self.aws_resources: _check_function_not_defined(resources_info) self._delete_resources(resources_info) - else: + else: resources_info = self.aws_resources[index] _check_function_not_defined(resources_info) - self._delete_resources(resources_info) + self._delete_resources(resources_info) @excp.exception(logger) def log(self): @@ -230,11 +231,8 @@ def log(self): if len(self.aws_resources) > 1: index = _choose_function(self.aws_resources) # We only return the logs of one function each time - if index >= 0: - aws_log = CloudWatchLogs(self.aws_resources[index]).get_aws_log() - batch_logs = self._get_batch_logs(self.aws_resources[index]) - aws_log += batch_logs if batch_logs else "" - logger.info(aws_log) + if index >= 0: + logger.info(CloudWatchLogs(self.aws_resources[index]).get_aws_logs()) @excp.exception(logger) def put(self): @@ -252,15 +250,15 @@ def get(self): def _create_api_gateway(self, resources_info: Dict): if resources_info.get("api_gateway", {}).get('name', False): APIGateway(resources_info).create_api_gateway() - + @excp.exception(logger) def _create_lambda_function(self, resources_info: Dict) -> None: lambda_client = Lambda(resources_info) response = lambda_client.create_function() response_parser.parse_lambda_function_creation_response(response, self.scar_info.get('cli_output'), - lambda_client.get_access_key()) - + lambda_client.get_access_key()) + @excp.exception(logger) def _create_log_group(self, resources_info: Dict) -> None: cloudwatch_logs = CloudWatchLogs(resources_info) @@ -268,7 +266,7 @@ def _create_log_group(self, resources_info: Dict) -> None: response_parser.parse_log_group_creation_response(response, cloudwatch_logs.get_log_group_name(), self.scar_info.get('cli_output')) - + @excp.exception(logger) def _create_s3_buckets(self, resources_info: Dict) -> None: if resources_info.get('lambda').get('input', False): @@ -277,19 +275,19 @@ def _create_s3_buckets(self, resources_info: Dict) -> None: if bucket.get('storage_provider') == 's3': bucket_name, _ = s3_service.create_bucket_and_folders(bucket.get('path')) Lambda(resources_info).link_function_and_bucket(bucket_name) - s3_service.set_input_bucket_notification(bucket_name) - + s3_service.set_input_bucket_notification(bucket_name) + if resources_info.get('lambda').get('output', False): - s3_service = Lambda(resources_info) + s3_service = S3(resources_info) for bucket in resources_info.get('lambda').get('output'): if bucket.get('storage_provider') == 's3': s3_service.create_bucket_and_folders(bucket.get('path')) - + @excp.exception(logger) def _add_api_gateway_permissions(self, resources_info: Dict): if resources_info.get("api_gateway").get('name', False): Lambda(resources_info).add_invocation_permission_from_api_gateway() - + @excp.exception(logger) def _create_batch_environment(self, resources_info: Dict) -> None: mode = resources_info.get('lambda').get('execution_mode') @@ -311,16 +309,16 @@ def _delete_resources(self, resources_info: Dict) -> None: self._delete_lambda_function(resources_info) # Delete resources batch self._delete_batch_resources(resources_info) - + def _delete_api_gateway(self, resources_info: Dict) -> None: - api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') + api_gateway_id = Lambda(resources_info).get_function_configuration().get('Environment').get('Variables').get('API_GATEWAY_ID') if api_gateway_id: resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id response = APIGateway(resources_info).delete_api_gateway() response_parser.parse_delete_api_response(response, api_gateway_id, self.scar_info.get('cli_output')) - + def _delete_logs(self, resources_info: Dict): cloudwatch_logs = CloudWatchLogs(resources_info) log_group_name = cloudwatch_logs.get_log_group_name(resources_info.get('lambda').get('name')) @@ -328,53 +326,28 @@ def _delete_logs(self, resources_info: Dict): response_parser.parse_delete_log_response(response, log_group_name, self.scar_info.get('cli_output')) - + def _delete_bucket_notifications(self, resources_info: Dict) -> None: if resources_info.get('lambda').get('input', False): for input_storage in resources_info.get('lambda').get('input'): if input_storage.get('storage_provider') == 's3': bucket_name = input_storage.get('path').split("/", 1)[0] S3(resources_info).delete_bucket_notification(bucket_name) - + def _delete_lambda_function(self, resources_info: Dict) -> None: response = Lambda(resources_info).delete_function() response_parser.parse_delete_function_response(response, resources_info.get('lambda').get('name'), self.scar_info.get('cli_output')) - + def _delete_batch_resources(self, resources_info: Dict) -> None: batch = Batch(resources_info) if batch.exist_compute_environments(): batch.delete_compute_environment() -############################################ -### ------------ ### -############################################ - - def _launch_lambda_function(self, resources_info): - response = Lambda(resources_info).launch_lambda_instance() - if self.scar_info.get("output_file", False): - response['OutputFile'] = self.scar_info.get("output_file") - response['OutputType'] = self.scar_info.get("cli_output") - response_parser.parse_invocation_response(**response) - - def _get_all_functions(self): - # Return the resources of the region in the scar's configuration file - resources_info = self.aws_resources[0] - arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id()) - return Lambda(resources_info).get_all_functions(arn_list) - - def _get_batch_logs(self, resources_info: Dict) -> str: - logs = "" - if resources_info.get('cloudwatch').get('request_id', False): - batch_jobs = Batch(resources_info).get_jobs_with_request_id() - logs = CloudWatchLogs(resources_info).get_batch_job_log(batch_jobs["jobs"]) - return logs - -# def _preheat_function(self): -# # If preheat is activated, the function is launched at the init step -# if hasattr(self.scar, "preheat"): -# self.aws_lambda.preheat_function() +########################################################### +### Methods to manage S3 resources ### +########################################################### def _process_s3_input_bucket_calls(self, resources_info: Dict, storage: Dict) -> None: s3_service = S3(resources_info) @@ -391,32 +364,6 @@ def _process_s3_input_bucket_calls(self, resources_info: Dict, storage: Dict) -> s3_event_list = s3_service.get_s3_event_list(bucket_name, s3_file_list) lambda_service.process_asynchronous_lambda_invocations(s3_event_list) - def _update_all_functions(self, lambda_functions): - for function_info in lambda_functions: - self.aws_lambda.update_function_configuration(function_info) - -# def _set_api_id(self, resources_info: Dict) -> None: -# api_gateway_id = Lambda(resources_info).get_function_info().get('Environment').get('Variables').get('API_GATEWAY_ID') -# if api_gateway_id: -# resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_gateway_id - -# def _update_local_function_properties(self, function_info): -# self._reset_aws_properties() -# """Update the defined properties with the AWS information.""" -# if function_info: -# self.aws_properties.lambdaf.update_properties(**function_info) -# if 'API_GATEWAY_ID' in self.aws_properties.lambdaf.environment['Variables']: -# api_gtw_id = self.aws_properties.lambdaf.environment['Variables'].get('API_GATEWAY_ID', -# "") -# if hasattr(self.aws_properties, 'api_gateway'): -# self.aws_properties.api_gateway.id = api_gtw_id -# else: -# self.aws_properties.api_gateway = ApiGatewayProperties({'id' : api_gtw_id}) - -####################################################### -### Methods to manage S3 files ### -####################################################### - def _upload_file_or_folder_to_s3(self, resources_info: Dict) -> None: path_to_upload = self.scar_info.get('path') files = [path_to_upload] @@ -435,7 +382,7 @@ def _get_download_file_path(self, file_key=None): return file_path def _download_file_or_folder_from_s3(self, resources_info: Dict) -> None: - + s3_service = S3(resources_info) s3_file_list = s3_service.get_bucket_file_list() for s3_file in s3_file_list: diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index e104c4c2..dbf9da3f 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -108,7 +108,8 @@ def link_function_and_bucket(self, bucket_name: str) -> None: def preheat_function(self): logger.info("Preheating function") self._set_request_response_call_parameters() - return self.launch_lambda_instance() + self.launch_lambda_instance() + logger.info("Preheating successful") def _launch_async_event(self, s3_event): self.set_asynchronous_call_parameters() @@ -137,7 +138,7 @@ def _launch_concurrent_lambda_invocations(self, s3_event_list): def launch_lambda_instance(self): if self.is_asynchronous(): - self.set_asynchronous_call_parameters() + self.set_asynchronous_call_parameters() response = self._invoke_lambda_function() response_args = {'Response' : response, 'FunctionName' : self.function.get('name'), @@ -173,52 +174,8 @@ def set_asynchronous_call_parameters(self): def _set_request_response_call_parameters(self): self.function.update(REQUEST_RESPONSE_CALL) - def _update_environment_variables(self, function_info, update_args): - 'TODO' -# # To update the environment variables we need to retrieve the -# # variables defined in lambda and update them with the new values -# env_vars = self.aws.lambdaf.environment -# if hasattr(self.aws.lambdaf, "environment_variables"): -# for env_var in self.aws.lambdaf.environment_variables: -# key_val = env_var.split("=") -# # Add an specific prefix to be able to find the variables defined by the user -# env_vars['Variables']['CONT_VAR_{0}'.format(key_val[0])] = key_val[1] -# if hasattr(self.aws.lambdaf, "timeout_threshold"): -# env_vars['Variables']['TIMEOUT_THRESHOLD'] = str(self.aws.lambdaf.timeout_threshold) -# if hasattr(self.aws.lambdaf, "log_level"): -# env_vars['Variables']['LOG_LEVEL'] = self.aws.lambdaf.log_level -# resources_info['Environment']['Variables'].update(env_vars['Variables']) -# update_args['Environment'] = resources_info['Environment'] - - def _update_supervisor_layer(self, function_info, update_args): - if hasattr(self.aws.lambdaf, "supervisor_layer"): - # Set supervisor layer Arn - function_layers = [self.layers.get_latest_supervisor_layer_arn()] - # Add the rest of layers (if exist) - if 'Layers' in function_info: - function_layers.extend([layer for layer in function_info['Layers'] if self.layers.layer_name not in layer['Arn']]) - update_args['Layers'] = function_layers - - def update_function_configuration(self, function_info=None): - 'TODO' -# if not resources_info: -# resources_info = self.get_function_info() -# update_args = {'FunctionName' : resources_info['FunctionName'] } -# # if hasattr(self.aws.lambdaf, "memory"): -# # update_args['MemorySize'] = self.aws.lambdaf.memory -# # else: -# # update_args['MemorySize'] = resources_info['MemorySize'] -# # if hasattr(self.aws.lambdaf, "time"): -# # update_args['Timeout'] = self.aws.lambdaf.time -# # else: -# # update_args['Timeout'] = resources_info['Timeout'] -# self._update_environment_variables(resources_info, update_args) -# self._update_supervisor_layer(resources_info, update_args) -# self.client.update_function_configuration(**update_args) -# logger.info("Function '{}' updated successfully.".format(resources_info['FunctionName'])) - def _get_function_environment_variables(self): - return self.get_function_info()['Environment'] + return self.get_function_configuration()['Environment'] def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict: result = ConfigFileParser().get_properties().get('aws') @@ -233,21 +190,21 @@ def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict: def get_all_functions(self, arn_list): try: - return [self.merge_aws_and_local_configuration(self.get_function_info(function_arn)) + return [self.merge_aws_and_local_configuration(self.get_function_configuration(function_arn)) for function_arn in arn_list] except ClientError as cerr: print (f"Error getting function info by arn: {cerr}") - def get_function_info(self, arn: str =None) -> Dict: + def get_function_configuration(self, arn: str=None) -> Dict: function = arn if arn else self.function.get('name') - return self.client.get_function_info(function) + return self.client.get_function_configuration(function) @excp.exception(logger) def find_function(self, function_name_or_arn=None): try: # If this call works the function exists name_arn = function_name_or_arn if function_name_or_arn else self.function.get('name', '') - self.get_function_info(name_arn) + self.get_function_configuration(name_arn) return True except ClientError as ce: # Function not found @@ -283,7 +240,6 @@ def _get_api_gateway_url(self): return self.resources_info.get('api_gateway').get('endpoint').format(api_id=api_id, api_region=self.resources_info.get('api_gateway').get('region'), stage_name=self.resources_info.get('api_gateway').get('stage_name')) - def call_http_endpoint(self): invoke_args = {'headers' : {'X-Amz-Invocation-Type':'Event'} if self.is_asynchronous() else {}} diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index cbcac8d4..8edd9954 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -18,6 +18,7 @@ from tabulate import tabulate import scar.logger as logger from scar.utils import StrUtils +from requests import Response class OutputType(Enum): @@ -26,10 +27,15 @@ class OutputType(Enum): VERBOSE = 3 BINARY = 4 - -def parse_http_response(response, function_name, asynch, output_type, output_file): +def parse_http_response(response: Response, resources_info: Dict, scar_info: Dict) -> None: + '''Process the response generated by an API Gateway invocation.''' + output_type = scar_info.get('cli_output') + function_name = resources_info.get('lambda').get('name') + asynch = resources_info.get('lambda').get('asynchronous') + text_message = "" if response.ok: if output_type == OutputType.BINARY.value: + output_file = scar_info.get('output_file', '') with open(output_file, "wb") as out: out.write(StrUtils.decode_base64(response.text)) text_message = f"Output saved in file '{output_file}'" @@ -43,7 +49,7 @@ def parse_http_response(response, function_name, asynch, output_type, output_fil text_message += StrUtils.base64_to_utf8_string(response.text) else: if asynch and response.status_code == 502: - text_message = f"Function '{function_name}' launched sucessfully." + text_message = f"Function '{function_name}' launched successfully." else: error = json.loads(response.text) if 'message' in error: @@ -122,7 +128,7 @@ def parse_ls_response(aws_resources: Dict, output_type: int) -> None: _print_generic_response('', output_type, aws_output, text_message, json_output=json_message, verbose_output=json_message) -def _parse_lambda_function_info(resources_info): +def _parse_lambda_function_info(resources_info: Dict) -> Dict: api_gateway = resources_info.get('lambda').get('environment').get('Variables').get('API_GATEWAY_ID', "-") if api_gateway != '-': stage_name = resources_info.get('api_gateway').get('stage_name') diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index acfb2037..b3a0d7db 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -34,7 +34,7 @@ def get_bucket_and_folders(storage_path: str) -> Tuple: class S3(GenericClient): def __init__(self, resources_info): - super().__init__() + super().__init__(resources_info.get('s3')) self.resources_info = resources_info @excp.exception(logger) diff --git a/scar/providers/aws/validators.py b/scar/providers/aws/validators.py index 6ff44db3..31c8f8bf 100644 --- a/scar/providers/aws/validators.py +++ b/scar/providers/aws/validators.py @@ -15,7 +15,6 @@ from scar.exceptions import ValidatorError, S3CodeSizeError, \ FunctionCodeSizeError, InvocationPayloadError -from scar.validator import GenericValidator from scar.utils import FileUtils, StrUtils VALID_LAMBDA_NAME_REGEX = (r"(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}(-gov)?-[a-z]+-\d{1}:)?(" @@ -26,10 +25,10 @@ MAX_POST_BODY_SIZE_ASYNC = KB * 95 -class AWSValidator(GenericValidator): +class AWSValidator(): """Class with methods to validate AWS properties.""" - @classmethod + @staticmethod def validate_kwargs(cls, **kwargs): aws_functions = kwargs.get('functions', {}).get('aws', {}) for function in aws_functions: @@ -49,7 +48,7 @@ def validate_iam(iam_properties): parameter_value=iam_properties, error_msg=error_msg) - @classmethod + @staticmethod def validate_lambda(cls, lambda_properties): if 'name' in lambda_properties: cls.validate_function_name(lambda_properties['name']) @@ -58,7 +57,7 @@ def validate_lambda(cls, lambda_properties): if 'time' in lambda_properties: cls.validate_time(lambda_properties['time']) - @classmethod + @staticmethod def validate_batch(cls, batch_properties): if 'vcpus' in batch_properties: cls.validate_batch_vcpus(batch_properties['vcpus']) diff --git a/scar/utils.py b/scar/utils.py index e2cc295f..77ee7fc5 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -24,6 +24,8 @@ import uuid import sys import yaml +from zipfile import ZipFile +from io import BytesIO from typing import Optional, Dict, List, Generator, Union, Any from distutils import dir_util from packaging import version @@ -204,6 +206,12 @@ def create_tmp_dir() -> tempfile.TemporaryDirectory: When the context is finished, the folder is automatically deleted.""" return tempfile.TemporaryDirectory() + @staticmethod + def create_tmp_file(**kwargs) -> tempfile.NamedTemporaryFile: + """Creates a directory in the temporal folder of the system. + When the context is finished, the folder is automatically deleted.""" + return tempfile.NamedTemporaryFile(**kwargs) + @staticmethod def get_tree_size(path: str) -> int: """Return total size of files in given path and subdirs.""" @@ -317,10 +325,15 @@ def create_tmp_config_file(cfg_args): def load_tmp_config_file(): return FileUtils.load_yaml(os.environ['SCAR_TMP_CFG']) - @staticmethod + @staticmethod def get_file_name(file_path: str) -> str: return os.path.basename(file_path) + @staticmethod + def extract_zip_from_url(url: str, dest_path: str) -> None: + with ZipFile(BytesIO(url)) as thezip: + thezip.extractall(dest_path) + class StrUtils: """Common methods for string management.""" diff --git a/scar/validator.py b/scar/validator.py deleted file mode 100644 index 0f9a3a40..00000000 --- a/scar/validator.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (C) GRyCAP - I3M - UPV -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import abc - -class GenericValidator(metaclass=abc.ABCMeta): - ''' All the different cloud provider validators must inherit - from this class to ensure that the commands are defined consistently''' - - @classmethod - def validate(cls): - ''' - A decorator that wraps the passed in function and validates the dictionary parameters passed - ''' - def decorator(func): - def wrapper(*args, **kwargs): - cls.validate_kwargs(**kwargs) - return func(*args, **kwargs) - return wrapper - return decorator - - @classmethod - @abc.abstractmethod - def validate_kwargs(**kwargs): - pass From 3d67a2bfbf8a8a467eb0f0958b8b52e8a6d9f16e Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Fri, 29 Nov 2019 11:51:15 +0100 Subject: [PATCH 17/41] Update 'storage_providers' variable name --- scar/parser/cli/__init__.py | 12 ++++++------ scar/parser/fdl.py | 6 +++--- scar/providers/aws/controller.py | 2 +- scar/providers/aws/functioncode.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index 714dd398..b737840d 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -111,9 +111,9 @@ def _parse_cloudwatchlogs_args(cmd_args: Dict) -> Dict: def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: s3_arg_list = ['deployment_bucket', - 'input_bucket', - 'output_bucket', - ('bucket', 'input_bucket')] + 'input_bucket', + 'output_bucket', + ('bucket', 'input_bucket')] s3_args = DataTypesUtils.parse_arg_list(s3_arg_list, cmd_args) storage = {} @@ -121,10 +121,10 @@ def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: if 'deployment_bucket' in s3_args: aws_args['lambda']['deployment'] = {'bucket': s3_args['deployment_bucket']} if 'input_bucket' in s3_args: - aws_args['lambda']['input'] = [{'storage_provider' : 's3', 'path': s3_args['input_bucket']}] + aws_args['lambda']['input'] = [{'storage_provider': 's3', 'path': s3_args['input_bucket']}] if 'output_bucket' in s3_args: - aws_args['lambda']['output'] = [{'storage_provider' : 's3', 'path': s3_args['output_bucket']}] - storage['storages'] = {'s3': []} + aws_args['lambda']['output'] = [{'storage_provider': 's3', 'path': s3_args['output_bucket']}] + storage['storage_providers'] = {'s3': []} return storage diff --git a/scar/parser/fdl.py b/scar/parser/fdl.py index 8445d062..24f7a179 100644 --- a/scar/parser/fdl.py +++ b/scar/parser/fdl.py @@ -31,12 +31,12 @@ def merge_cmd_yaml(cmd: Dict, yaml: Dict) -> Dict: result = yaml.copy() # We merge the cli commands with all the defined functions # CLI only allows define AWS parameters - for cli_cmd in cmd.get('functions', {}).get("aws",{}): + for cli_cmd in cmd.get('functions', {}).get("aws", {}): for index, function in enumerate(result.get('functions', {}).get("aws", {})): result['functions']['aws'][index] = \ DataTypesUtils.merge_dicts_with_copy(function, cli_cmd) result['scar'] = DataTypesUtils.merge_dicts_with_copy(result.get('scar', {}), cmd.get('scar', {})) - result['storages'] = DataTypesUtils.merge_dicts_with_copy(result.get('storages', {}), - cmd.get('storages', {})) + result['storage_providers'] = DataTypesUtils.merge_dicts_with_copy(result.get('storage_providers', {}), + cmd.get('storage_providers', {})) return result diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index a1047d49..14f98052 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -131,7 +131,7 @@ def __init__(self, func_call): self.raw_args = FileUtils.load_tmp_config_file() AWSValidator.validate_kwargs(self.raw_args) self.aws_resources = self.raw_args.get('functions', {}).get('aws', {}) - self.storages = self.raw_args.get('storages', {}) + self.storage_providers = self.raw_args.get('storage_providers', {}) self.scar_info = self.raw_args.get('scar', {}) _add_extra_aws_properties(self.scar_info, self.aws_resources) # Call the user's command diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 7c859e04..5c44f20a 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -69,7 +69,7 @@ def _extract_handler_code(self) -> None: def _copy_function_configuration(self): cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") - function_cfg = {"storages" : FileUtils.load_tmp_config_file().get('storages', {})} + function_cfg = {"storage_providers": FileUtils.load_tmp_config_file().get('storage_providers', {})} function_cfg.update(self.resources_info['lambda']) FileUtils.write_yaml(cfg_file_path, function_cfg) From 217e95262bca39a30ba6e45dd370e75e99c44c87 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Fri, 29 Nov 2019 12:51:12 +0100 Subject: [PATCH 18/41] Add FDL config file to batch job definition --- scar/parser/cli/__init__.py | 2 +- scar/parser/fdl.py | 2 +- scar/providers/aws/batchfunction.py | 10 ++++++++++ scar/providers/aws/functioncode.py | 9 +++++++-- scar/providers/aws/lambdafunction.py | 10 +++++----- scar/utils.py | 8 ++++---- scar/version.py | 2 +- 7 files changed, 29 insertions(+), 14 deletions(-) diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index b737840d..777a4346 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -124,7 +124,7 @@ def _parse_s3_args(aws_args: Dict, cmd_args: Dict) -> Dict: aws_args['lambda']['input'] = [{'storage_provider': 's3', 'path': s3_args['input_bucket']}] if 'output_bucket' in s3_args: aws_args['lambda']['output'] = [{'storage_provider': 's3', 'path': s3_args['output_bucket']}] - storage['storage_providers'] = {'s3': []} + storage['storage_providers'] = {'s3': {}} return storage diff --git a/scar/parser/fdl.py b/scar/parser/fdl.py index 24f7a179..fa4b19c6 100644 --- a/scar/parser/fdl.py +++ b/scar/parser/fdl.py @@ -38,5 +38,5 @@ def merge_cmd_yaml(cmd: Dict, yaml: Dict) -> Dict: result['scar'] = DataTypesUtils.merge_dicts_with_copy(result.get('scar', {}), cmd.get('scar', {})) result['storage_providers'] = DataTypesUtils.merge_dicts_with_copy(result.get('storage_providers', {}), - cmd.get('storage_providers', {})) + cmd.get('storage_providers', {})) return result diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index 91da5ef5..d7e2396d 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -13,9 +13,11 @@ # limitations under the License. from typing import Dict, List +import yaml from scar.providers.aws import GenericClient import scar.logger as logger from scar.providers.aws.launchtemplates import LaunchTemplates +from scar.providers.aws.functioncode import create_function_config from scar.utils import FileUtils, StrUtils @@ -35,6 +37,7 @@ def __init__(self, resources_info): def _set_required_environment_variables(self) -> None: self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self.function_name) self._set_batch_environment_variable('SCRIPT', self._get_user_script()) + self._set_batch_environment_variable('FUNCTION_CONFIG', self._get_config_file()) if self.resources_info.get('lambda').get('container').get('environment').get('Variables', False): for key, value in self.resources_info.get('lambda').get('container').get('environment').get('Variables').items(): self._set_batch_environment_variable(key, value) @@ -49,6 +52,13 @@ def _get_user_script(self) -> str: script = StrUtils.utf8_to_base64_string(file_content) return script + def _get_config_file(self) -> str: + cfg_file = '' + config = create_function_config(self.resources_info) + yaml_str = yaml.safe_dump(config) + cfg_file = StrUtils.utf8_to_base64_string(yaml_str) + return cfg_file + def _delete_job_definitions(self) -> None: # Get main job definition kwargs = {"jobDefinitionName": self.function_name} diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 5c44f20a..e62b9a0a 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -23,6 +23,12 @@ from scar.utils import FileUtils, GitHubUtils, \ GITHUB_USER, GITHUB_SUPERVISOR_PROJECT + +def create_function_config(resources_info): + function_cfg = {'storage_providers': FileUtils.load_tmp_config_file().get('storage_providers', {})} + function_cfg.update(resources_info.get('lambda')) + return function_cfg + class FunctionPackager(): """Class to manage the deployment package creation.""" @@ -69,8 +75,7 @@ def _extract_handler_code(self) -> None: def _copy_function_configuration(self): cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") - function_cfg = {"storage_providers": FileUtils.load_tmp_config_file().get('storage_providers', {})} - function_cfg.update(self.resources_info['lambda']) + function_cfg = create_function_config(self.resources_info) FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index dbf9da3f..7975dbea 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -88,8 +88,8 @@ def _get_function_code(self, zip_payload_path: str) -> Dict: if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), - file_path=zip_payload_path, - file_key=file_key) + file_path=zip_payload_path, + file_key=file_key) code = {"S3Bucket": self.function.get('deployment_bucket'), "S3Key": file_key} else: @@ -100,9 +100,9 @@ def delete_function(self): return self.client.delete_function(self.resources_info.get('lambda').get('name')) def link_function_and_bucket(self, bucket_name: str) -> None: - kwargs = {'FunctionName' : self.function.get('name'), - 'Principal' : "s3.amazonaws.com", - 'SourceArn' : f'arn:aws:s3:::{bucket_name}'} + kwargs = {'FunctionName': self.function.get('name'), + 'Principal': "s3.amazonaws.com", + 'SourceArn': f'arn:aws:s3:::{bucket_name}'} self.client.add_invocation_permission(**kwargs) def preheat_function(self): diff --git a/scar/utils.py b/scar/utils.py index 77ee7fc5..b4c24436 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -309,8 +309,8 @@ def load_yaml(file_path: str) -> Dict: return yaml.safe_load(cfg_file) else: raise YamlFileNotFoundError(file_path=file_path) - - @staticmethod + + @staticmethod def write_yaml(file_path: str, content: Dict) -> None: with open(file_path, 'w') as cfg_file: yaml.safe_dump(content, cfg_file) @@ -320,11 +320,11 @@ def create_tmp_config_file(cfg_args): cfg_path = FileUtils.join_paths(SysUtils.get_user_home_path(), ".scar", "scar_tmp.yaml") os.environ['SCAR_TMP_CFG'] = cfg_path FileUtils.write_yaml(cfg_path, cfg_args) - + @staticmethod def load_tmp_config_file(): return FileUtils.load_yaml(os.environ['SCAR_TMP_CFG']) - + @staticmethod def get_file_name(file_path: str) -> str: return os.path.basename(file_path) diff --git a/scar/version.py b/scar/version.py index b34bc1f3..12415c36 100644 --- a/scar/version.py +++ b/scar/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '3.2.4' \ No newline at end of file +__version__ = '3.2.4' From a74caf978e090bd0e7c76f6abf43a7c3da7e38ea Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Fri, 29 Nov 2019 13:20:35 +0100 Subject: [PATCH 19/41] Fix supervisor download in /tmp (#331) --- scar/providers/aws/functioncode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index e62b9a0a..75fc8558 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -37,7 +37,7 @@ def __init__(self, resources_info: Dict): # Temporal folder to store the supervisor and udocker files self._tmp_payload_folder = FileUtils.create_tmp_dir() # Path where the supervisor is downloaded - self._supervisor_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), 'faas.zip') + self._supervisor_zip_path = FileUtils.join_paths(FileUtils.create_tmp_dir().name, 'faas.zip') @exception(logger) def create_zip(self, lambda_payload_path: str) -> None: From 768cef4aa1d6c5c047ceb486db7c073821a24792 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Wed, 4 Dec 2019 11:59:11 +0100 Subject: [PATCH 20/41] Fix supervisor tmp folder creation --- scar/providers/aws/functioncode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 75fc8558..b6ea8a2e 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -37,7 +37,8 @@ def __init__(self, resources_info: Dict): # Temporal folder to store the supervisor and udocker files self._tmp_payload_folder = FileUtils.create_tmp_dir() # Path where the supervisor is downloaded - self._supervisor_zip_path = FileUtils.join_paths(FileUtils.create_tmp_dir().name, 'faas.zip') + self._supervisor_path = FileUtils.create_tmp_dir() + self._supervisor_zip_path = FileUtils.join_paths(self._supervisor_path.name, 'faas.zip') @exception(logger) def create_zip(self, lambda_payload_path: str) -> None: From c48545ceed1f2c30591aaff6643a866306a6da07 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Wed, 4 Dec 2019 13:15:37 +0100 Subject: [PATCH 21/41] Fix invoke response --- scar/providers/aws/controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 14f98052..838b55d8 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -164,10 +164,8 @@ def invoke(self): resources_info = self.aws_resources[index] response = Lambda(resources_info).call_http_endpoint() response_parser.parse_http_response(response, - resources_info.get('lambda').get('name'), - resources_info.get('lambda').get('asynchronous'), - self.scar_info.get('cli_output'), - self.scar_info.get('output_file', '')) + resources_info, + self.scar_info) @excp.exception(logger) def run(self): From 875aadfe51274dc56c9309d616f509de953dc136 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Thu, 5 Dec 2019 17:53:22 +0100 Subject: [PATCH 22/41] Fixes definition of batch resources --- scar/providers/aws/batchfunction.py | 8 ++++---- scar/providers/aws/functioncode.py | 2 +- scar/providers/aws/lambdalayers.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index d7e2396d..de3111e1 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -43,7 +43,7 @@ def _set_required_environment_variables(self) -> None: self._set_batch_environment_variable(key, value) def _set_batch_environment_variable(self, key: str, value: str) -> None: - self.resources_info['batch']['environment']['Variables'].update({'name': key, 'value': value}) + self.resources_info['batch']['environment']['Variables'].update({key: value}) def _get_user_script(self) -> str: script = '' @@ -146,7 +146,7 @@ def _get_creations_job_queue_args(self): 'order': 1}, ], 'jobQueueName': self.function_name, 'priority': 1, - 'state': self.batch.get('compute_resources').get('state'), + 'state': self.batch.get('state'), } def _get_job_definition_args(self): @@ -154,7 +154,7 @@ def _get_job_definition_args(self): 'jobDefinitionName': self.function_name, 'type': 'container', 'containerProperties': { - 'image': self.resources_info.get('lambda').get('image'), + 'image': self.resources_info.get('lambda').get('container').get('image'), 'memory': int(self.batch.get('memory')), 'vcpus': int(self.batch.get('vcpus')), 'command': [ @@ -170,7 +170,7 @@ def _get_job_definition_args(self): 'name': 'supervisor-bin' } ], - 'environment': [{key, value} for key, value in self.resources_info['batch']['environment']['Variables'].items()], + 'environment': [{'name': key, 'value': value} for key, value in self.resources_info['batch']['environment']['Variables'].items()], 'mountPoints': [ { 'containerPath': '/opt/faas-supervisor/bin', diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index b6ea8a2e..3367f42f 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -94,7 +94,7 @@ def _add_init_script(self) -> None: FileUtils.copy_file(init_script_path, FileUtils.join_paths(self._tmp_payload_folder.name, FileUtils.get_file_name(init_script_path))) - del(self.resources_info['lambda']['init_script']) + #del(self.resources_info['lambda']['init_script']) def _add_extra_payload(self) -> None: if self.resources_info.get('lambda').get('extra_payload', False): diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index 6ef721b0..594f6dff 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -114,7 +114,7 @@ def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: return {'LayerName' : self.layer_name, 'Description' : self.supervisor_version, 'Content' : {'ZipFile': FileUtils.read_file(layer_zip_path, mode="rb")}, - 'LicenseInfo' : self.resources_info.get('supervisor').get('license_info')} + 'LicenseInfo' : self.resources_info.get('lambda').get('supervisor').get('license_info')} def _create_layer(self) -> None: tmp_zip_path, layer_code_path = _create_tmp_folders() From 8f597035ab9d0233b4ee3c096fe062a5b5c029e5 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Mon, 9 Dec 2019 17:45:35 +0100 Subject: [PATCH 23/41] WIP: Unify supervisor version management --- scar/providers/aws/controller.py | 5 ++- scar/providers/aws/functioncode.py | 44 ++++++++--------------- scar/providers/aws/lambdafunction.py | 27 +++++++++------ scar/providers/aws/launchtemplates.py | 16 ++------- scar/utils.py | 50 +++++++++++++++++++++++++-- 5 files changed, 85 insertions(+), 57 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 838b55d8..2e68b8e2 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -28,7 +28,7 @@ import scar.exceptions as excp import scar.logger as logger import scar.providers.aws.response as response_parser -from scar.utils import StrUtils, FileUtils +from scar.utils import StrUtils, FileUtils, SupervisorUtils _ACCOUNT_ID_REGEX = r'\d{12}' @@ -147,6 +147,9 @@ def init(self) -> None: _check_function_defined(resources_info) # We have to create the gateway before creating the function self._create_api_gateway(resources_info) + # Check the specified supervisor version + resources_info['lambda']['supervisor']['version'] = SupervisorUtils.check_supervisor_version( + resources_info.get('lambda').get('supervisor').get('version')) self._create_lambda_function(resources_info) self._create_log_group(resources_info) self._create_s3_buckets(resources_info) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 3367f42f..d2f4b2ee 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -19,9 +19,7 @@ from scar.providers.aws.validators import AWSValidator from scar.exceptions import exception import scar.logger as logger -from scar.http.request import get_file -from scar.utils import FileUtils, GitHubUtils, \ - GITHUB_USER, GITHUB_SUPERVISOR_PROJECT +from scar.utils import FileUtils def create_function_config(resources_info): @@ -32,18 +30,15 @@ def create_function_config(resources_info): class FunctionPackager(): """Class to manage the deployment package creation.""" - def __init__(self, resources_info: Dict): + def __init__(self, resources_info: Dict, supervisor_zip_path: str): self.resources_info = resources_info + self.supervisor_zip_path = supervisor_zip_path # Temporal folder to store the supervisor and udocker files - self._tmp_payload_folder = FileUtils.create_tmp_dir() - # Path where the supervisor is downloaded - self._supervisor_path = FileUtils.create_tmp_dir() - self._supervisor_zip_path = FileUtils.join_paths(self._supervisor_path.name, 'faas.zip') + self.tmp_payload_folder = FileUtils.create_tmp_dir() @exception(logger) def create_zip(self, lambda_payload_path: str) -> None: """Creates the lambda function deployment package.""" - self._download_faas_supervisor_zip() self._extract_handler_code() self._copy_function_configuration() self._manage_udocker_images() @@ -52,18 +47,10 @@ def create_zip(self, lambda_payload_path: str) -> None: self._zip_scar_folder(lambda_payload_path) self._check_code_size() - def _download_faas_supervisor_zip(self) -> None: - supervisor_zip_url = GitHubUtils.get_source_code_url( - GITHUB_USER, - GITHUB_SUPERVISOR_PROJECT, - self.resources_info.get('lambda').get('supervisor').get('version')) - with open(self._supervisor_zip_path, "wb") as thezip: - thezip.write(get_file(supervisor_zip_url)) - def _extract_handler_code(self) -> None: - function_handler_dest = FileUtils.join_paths(self._tmp_payload_folder.name, f"{self.resources_info.get('lambda').get('name')}.py") + function_handler_dest = FileUtils.join_paths(self.tmp_payload_folder.name, f"{self.resources_info.get('lambda').get('name')}.py") file_path = "" - with ZipFile(self._supervisor_zip_path) as thezip: + with ZipFile(self.supervisor_zip_path) as thezip: for file in thezip.namelist(): if file.endswith("function_handler.py"): file_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), file) @@ -75,16 +62,16 @@ def _extract_handler_code(self) -> None: FileUtils.copy_file(file_path, function_handler_dest) def _copy_function_configuration(self): - cfg_file_path = FileUtils.join_paths(self._tmp_payload_folder.name, "function_config.yaml") + cfg_file_path = FileUtils.join_paths(self.tmp_payload_folder.name, "function_config.yaml") function_cfg = create_function_config(self.resources_info) FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): if self.resources_info.get('lambda').get('deployment').get('bucket', False) and \ self.resources_info.get('lambda').get('container').get('image', False): - Udocker(self.resources_info, self._tmp_payload_folder.name, self._supervisor_zip_path).download_udocker_image() + Udocker(self.resources_info, self.tmp_payload_folder.name, self.supervisor_zip_path).download_udocker_image() if self.resources_info.get('lambda').get('image_file'): - Udocker(self.resources_info, self._tmp_payload_folder.name, self._supervisor_zip_path).prepare_udocker_image() + Udocker(self.resources_info, self.tmp_payload_folder.name, self.supervisor_zip_path).prepare_udocker_image() del(self.resources_info['lambda']['image_file']) def _add_init_script(self) -> None: @@ -92,9 +79,8 @@ def _add_init_script(self) -> None: if self.resources_info.get('lambda').get('init_script', False): init_script_path = self.resources_info.get('lambda').get('init_script') FileUtils.copy_file(init_script_path, - FileUtils.join_paths(self._tmp_payload_folder.name, + FileUtils.join_paths(self.tmp_payload_folder.name, FileUtils.get_file_name(init_script_path))) - #del(self.resources_info['lambda']['init_script']) def _add_extra_payload(self) -> None: if self.resources_info.get('lambda').get('extra_payload', False): @@ -102,24 +88,24 @@ def _add_extra_payload(self) -> None: logger.info(f"Adding extra payload '{payload_path}'") if FileUtils.is_file(payload_path): FileUtils.copy_file(self.resources_info.get('lambda').get('extra_payload'), - self._tmp_payload_folder.name) + self.tmp_payload_folder.name) else: FileUtils.copy_dir(self.resources_info.get('lambda').get('extra_payload'), - self._tmp_payload_folder.name) + self.tmp_payload_folder.name) del(self.resources_info['lambda']['extra_payload']) def _zip_scar_folder(self, lambda_payload_path: str) -> None: """Zips the tmp folder with all the function's files and save it in the expected path of the payload.""" FileUtils.zip_folder(lambda_payload_path, - self._tmp_payload_folder.name, + self.tmp_payload_folder.name, "Creating function package") def _check_code_size(self): # Check if the code size fits within the AWS limits if self.resources_info.get('lambda').get('deployment').get('bucket', False): - AWSValidator.validate_s3_code_size(self._tmp_payload_folder.name, + AWSValidator.validate_s3_code_size(self.tmp_payload_folder.name, self.resources_info.get('lambda').get('deployment').get('max_s3_payload_size')) else: - AWSValidator.validate_function_code_size(self._tmp_payload_folder.name, + AWSValidator.validate_function_code_size(self.tmp_payload_folder.name, self.resources_info.get('lambda').get('deployment').get('max_payload_size')) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 7975dbea..b3cee14c 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -24,7 +24,7 @@ from scar.providers.aws.validators import AWSValidator import scar.exceptions as excp import scar.logger as logger -from scar.utils import DataTypesUtils, FileUtils, StrUtils +from scar.utils import DataTypesUtils, FileUtils, StrUtils, SupervisorUtils from typing import Dict from scar.parser.cfgfile import ConfigFileParser @@ -43,6 +43,12 @@ def __init__(self, resources_info: Dict) -> None: super().__init__(resources_info.get('lambda', {})) self.resources_info = resources_info self.function = resources_info.get('lambda', {}) + supervisor_version = resources_info.get('lambda').get('supervisor').get('version') + self.supervisor_path = FileUtils.create_tmp_dir() + self.supervisor_zip_path = SupervisorUtils.download_supervisor( + supervisor_version, + self.supervisor_path.name + ) def _get_creations_args(self, zip_payload_path: str) -> Dict: return {'FunctionName': self.function.get('name'), @@ -76,15 +82,14 @@ def create_function(self): return response def _manage_supervisor_layer(self): - layers_client = LambdaLayers(self.resources_info, self.client) - layers_client.check_faas_supervisor_layer() - self.function.get('layers', []).append(layers_client.get_latest_supervisor_layer_arn()) + layers_client = LambdaLayers(self.resources_info, self.client, self.supervisor_zip_path) + self.function.get('layers', []).append(layers_client.get_supervisor_layer_arn()) @excp.exception(logger) def _get_function_code(self, zip_payload_path: str) -> Dict: '''Zip all the files and folders needed.''' code = {} - FunctionPackager(self.resources_info).create_zip(zip_payload_path) + FunctionPackager(self.resources_info, self.supervisor_zip_path).create_zip(zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), @@ -154,18 +159,18 @@ def _get_invocation_payload(self): script_path = self.function.get("run_script") # We first code to base64 in bytes and then decode those bytes to allow the json lib to parse the data # https://stackoverflow.com/questions/37225035/serialize-in-json-a-base64-encoded-data#37239382 - payload = { "script" : StrUtils.bytes_to_base64str(FileUtils.read_file(script_path, 'rb')) } + payload = {"script": StrUtils.bytes_to_base64str(FileUtils.read_file(script_path, 'rb'))} # Check for defined commands # This overrides any other function payload if self.function.get("c_args", False): - payload = {"cmd_args" : json.dumps(self.function.get("c_args"))} + payload = {"cmd_args": json.dumps(self.function.get("c_args"))} return json.dumps(payload) def _invoke_lambda_function(self): - invoke_args = {'FunctionName' : self.function.get('name'), - 'InvocationType' : self.function.get('invocation_type'), - 'LogType' : self.function.get('log_type'), - 'Payload' : self._get_invocation_payload()} + invoke_args = {'FunctionName': self.function.get('name'), + 'InvocationType': self.function.get('invocation_type'), + 'LogType': self.function.get('log_type'), + 'Payload': self._get_invocation_payload()} return self.client.invoke_function(**invoke_args) def set_asynchronous_call_parameters(self): diff --git a/scar/providers/aws/launchtemplates.py b/scar/providers/aws/launchtemplates.py index 9e4afc63..685bad38 100644 --- a/scar/providers/aws/launchtemplates.py +++ b/scar/providers/aws/launchtemplates.py @@ -35,7 +35,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from scar.providers.aws import GenericClient -from scar.utils import GitHubUtils, StrUtils +from scar.utils import SupervisorUtils, StrUtils import scar.exceptions as excp import scar.logger as logger @@ -43,10 +43,6 @@ class LaunchTemplates(GenericClient): """Class to manage the creation and update of launch templates.""" - _SUPERVISOR_GITHUB_REPO = 'faas-supervisor' - _SUPERVISOR_GITHUB_USER = 'grycap' - _SUPERVISOR_GITHUB_ASSET_NAME = 'supervisor' - # Script to download 'faas-supervisor' _LAUNCH_TEMPLATE_SCRIPT = Template( '#!/bin/bash\n' @@ -59,11 +55,6 @@ def __init__(self, resources_info: Dict): super().__init__(resources_info.get('batch')) self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version') self.template_name = resources_info.get('batch').get('compute_resources').get('launch_template_name') - if self.supervisor_version == 'latest': - self.supervisor_version = GitHubUtils.get_latest_release( - self._SUPERVISOR_GITHUB_USER, - self._SUPERVISOR_GITHUB_REPO - ) @excp.exception(logger) def _is_supervisor_created(self) -> bool: @@ -117,10 +108,7 @@ def _create_supervisor_user_data(self) -> str: chmod +x /opt/faas-supervisor/bin/supervisor --===============3595946014116037730==--""" multipart = MIMEMultipart() - url = GitHubUtils.get_asset_url(self._SUPERVISOR_GITHUB_USER, - self._SUPERVISOR_GITHUB_REPO, - self._SUPERVISOR_GITHUB_ASSET_NAME, - self.supervisor_version) + url = SupervisorUtils.get_supervisor_binary_url(self.supervisor_version) script = self._LAUNCH_TEMPLATE_SCRIPT.substitute( supervisor_binary_url=url) content = MIMEText(script, 'x-shellscript') diff --git a/scar/utils.py b/scar/utils.py index b4c24436..9ed352d3 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -33,8 +33,6 @@ import scar.http.request as request from scar.exceptions import GitHubTagNotFoundError, YamlFileNotFoundError -GITHUB_USER = 'grycap' -GITHUB_SUPERVISOR_PROJECT = 'faas-supervisor' COMMANDS = ['scar-config'] def lazy_property(func): @@ -455,3 +453,51 @@ def get_source_code_url(user: str, project: str, tag_name: str='latest') -> str: if isinstance(response, dict): source_url = response.get('zipball_url') return source_url + + +class SupervisorUtils: + """Common methods for FaaS Supervisor management. + https://github.com/grycap/faas-supervisor/""" + + _SUPERVISOR_GITHUB_REPO = 'faas-supervisor' + _SUPERVISOR_GITHUB_USER = 'grycap' + _SUPERVISOR_GITHUB_ASSET_NAME = 'supervisor' + + @classmethod + def download_supervisor(cls, supervisor_version: str, path: str) -> str: + """Downloads the FaaS Supervisor .zip package to the specified path.""" + supervisor_zip_path = FileUtils.join_paths(path, 'faas-supervisor.zip') + supervisor_zip_url = GitHubUtils.get_source_code_url( + cls._SUPERVISOR_GITHUB_USER, + cls._SUPERVISOR_GITHUB_REPO, + supervisor_version) + with open(supervisor_zip_path, "wb") as thezip: + thezip.write(request.get_file(supervisor_zip_url)) + return supervisor_zip_path + + @classmethod + def check_supervisor_version(cls, supervisor_version: str) -> str: + """Checks if the specified version exists in FaaS Supervisor's GitHub + repository. Returns the version if exists and 'latest' if not.""" + if GitHubUtils.exists_release_in_repo(cls._SUPERVISOR_GITHUB_USER, + cls._SUPERVISOR_GITHUB_REPO, + supervisor_version): + return version + latest_version = SupervisorUtils.get_latest_release() + logger.info(('Defined supervisor version does not exists. ' + f'Using latest release: {latest_version}.')) + return latest_version + + @classmethod + def get_supervisor_binary_url(cls, supervisor_version: str) -> str: + """Returns the supervisor's binary download url.""" + return GitHubUtils.get_asset_url(cls._SUPERVISOR_GITHUB_USER, + cls._SUPERVISOR_GITHUB_REPO, + cls._SUPERVISOR_GITHUB_ASSET_NAME, + supervisor_version) + + @classmethod + def get_latest_release(cls) -> str: + """Returns the latest FaaS Supervisor version.""" + return GitHubUtils.get_latest_release(cls._SUPERVISOR_GITHUB_USER, + cls._SUPERVISOR_GITHUB_REPO) From aa7c6d84c5e96f7853728546fef642aa15dc59d7 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Tue, 10 Dec 2019 13:26:48 +0100 Subject: [PATCH 24/41] Unify supervisor version management --- scar/providers/aws/clients/lambdafunction.py | 15 ++ scar/providers/aws/lambdafunction.py | 6 +- scar/providers/aws/lambdalayers.py | 155 ++++++++----------- scar/utils.py | 2 +- 4 files changed, 82 insertions(+), 96 deletions(-) diff --git a/scar/providers/aws/clients/lambdafunction.py b/scar/providers/aws/clients/lambdafunction.py index 368a5fd0..62383106 100644 --- a/scar/providers/aws/clients/lambdafunction.py +++ b/scar/providers/aws/clients/lambdafunction.py @@ -95,6 +95,21 @@ def list_layers(self, next_token: Optional[str]=None) -> List: layers.extend(self.list_layers(next_token=layers_info['NextMarker'])) return layers + @excp.exception(logger) + def list_layer_versions(self, layer_name: str, next_token: Optional[str]=None) -> str: + """Lists the versions of an AWS Lambda layer.""" + logger.debug(f'Listing versions of lambda layer "{layer_name}".') + versions = [] + kwargs = {'LayerName': layer_name} + if next_token: + kwargs['Marker'] = next_token + layer_versions_info = self.client.list_layer_version(**kwargs) + if 'LayerVersions' in layer_versions_info and layer_versions_info['LayerVersions']: + versions.extend(layer_versions_info['LayerVersions']) + if 'NextMarker' in layer_versions_info: + versions.extend(self.list_layer_versions(layer_name, next_token=layer_versions_info['NextMarker'])) + return versions + @excp.exception(logger) def delete_function(self, function_name: str) -> Dict: """Deletes the specified Lambda diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index b3cee14c..ef6640e3 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -14,9 +14,10 @@ import base64 import json -from scar.http.request import call_http_endpoint +from typing import Dict from multiprocessing.pool import ThreadPool from botocore.exceptions import ClientError +from scar.http.request import call_http_endpoint from scar.providers.aws import GenericClient from scar.providers.aws.functioncode import FunctionPackager from scar.providers.aws.lambdalayers import LambdaLayers @@ -25,7 +26,6 @@ import scar.exceptions as excp import scar.logger as logger from scar.utils import DataTypesUtils, FileUtils, StrUtils, SupervisorUtils -from typing import Dict from scar.parser.cfgfile import ConfigFileParser MAX_CONCURRENT_INVOCATIONS = 500 @@ -198,7 +198,7 @@ def get_all_functions(self, arn_list): return [self.merge_aws_and_local_configuration(self.get_function_configuration(function_arn)) for function_arn in arn_list] except ClientError as cerr: - print (f"Error getting function info by arn: {cerr}") + print(f"Error getting function info by arn: {cerr}") def get_function_configuration(self, arn: str=None) -> Dict: function = arn if arn else self.function.get('name') diff --git a/scar/providers/aws/lambdalayers.py b/scar/providers/aws/lambdalayers.py index 594f6dff..f85d5a58 100644 --- a/scar/providers/aws/lambdalayers.py +++ b/scar/providers/aws/lambdalayers.py @@ -12,55 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. """Module with methods and classes to manage the Lambda layers.""" - -import io import shutil -from typing import Dict - +from typing import Dict, List import zipfile -import scar.http.request as request import scar.logger as logger -from scar.utils import FileUtils, GitHubUtils, StrUtils, \ - GITHUB_USER, GITHUB_SUPERVISOR_PROJECT +from scar.utils import FileUtils from scar.providers.aws.clients.lambdafunction import LambdaClient -def _create_tmp_folders() -> None: - tmp_zip_folder = FileUtils.create_tmp_dir() - layer_code_folder = FileUtils.create_tmp_dir() - return (tmp_zip_folder.name, layer_code_folder.name) - - -def _download_supervisor(supervisor_version: str, tmp_zip_path: str) -> str: - supervisor_zip_url = GitHubUtils.get_source_code_url(GITHUB_USER, - GITHUB_SUPERVISOR_PROJECT, - supervisor_version) - supervisor_zip = request.get_file(supervisor_zip_url) - with zipfile.ZipFile(io.BytesIO(supervisor_zip)) as thezip: - for file in thezip.namelist(): - # Remove the parent folder path - parent_folder, file_name = file.split("/", 1) - if file_name.startswith("extra") or file_name.startswith("faassupervisor"): - thezip.extract(file, tmp_zip_path) - return parent_folder - - -def _copy_supervisor_files(parent_folder: str, tmp_zip_path: str, layer_code_path: str) -> None: - supervisor_path = FileUtils.join_paths(tmp_zip_path, parent_folder, 'faassupervisor') - shutil.move(supervisor_path, FileUtils.join_paths(layer_code_path, 'python', 'faassupervisor')) - - -def _copy_extra_files(parent_folder: str, tmp_zip_path: str, layer_code_path: str) -> None: - extra_folder_path = FileUtils.join_paths(tmp_zip_path, parent_folder, 'extra') - files = FileUtils.get_all_files_in_directory(extra_folder_path) - for file_path in files: - FileUtils.unzip_folder(file_path, layer_code_path) - - -def _create_layer_zip(layer_zip_path: str, layer_code_path: str) -> None: - FileUtils.zip_folder(layer_zip_path, layer_code_path) - - class Layer(): """Class used for layer management.""" @@ -84,9 +43,12 @@ def exists(self, layer_name: str) -> bool: return True return False + def list_versions(self, layer_name: str) -> List: + return self.lambda_client.list_layer_versions(layer_name) + def delete(self, **kwargs: Dict) -> Dict: """Deletes a layer.""" - layer_args = {'LayerName' : kwargs['name']} + layer_args = {'LayerName': kwargs['name']} if 'version' in kwargs: layer_args['VersionNumber'] = int(kwargs['version']) else: @@ -104,57 +66,66 @@ class LambdaLayers(): """"Class used to manage the lambda supervisor layer.""" # To avoid circular inheritance we need to receive the LambdaClient - def __init__(self, resources_info: Dict, lambda_client: LambdaClient): + def __init__(self, resources_info: Dict, lambda_client: LambdaClient, supervisor_zip_path: str): self.resources_info = resources_info + self.supervisor_zip_path = supervisor_zip_path self.layer_name = resources_info.get('lambda').get('supervisor').get('layer_name') self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version') self.layer = Layer(lambda_client) def _get_supervisor_layer_props(self, layer_zip_path: str) -> Dict: - return {'LayerName' : self.layer_name, - 'Description' : self.supervisor_version, - 'Content' : {'ZipFile': FileUtils.read_file(layer_zip_path, mode="rb")}, - 'LicenseInfo' : self.resources_info.get('lambda').get('supervisor').get('license_info')} - - def _create_layer(self) -> None: - tmp_zip_path, layer_code_path = _create_tmp_folders() - layer_zip_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), f"{self.layer_name}.zip") - parent_folder = _download_supervisor(self.supervisor_version, tmp_zip_path) - _copy_supervisor_files(parent_folder, tmp_zip_path, layer_code_path) - _copy_extra_files(parent_folder, tmp_zip_path, layer_code_path) - _create_layer_zip(layer_zip_path, layer_code_path) - self.layer.create(**self._get_supervisor_layer_props(layer_zip_path)) - FileUtils.delete_file(layer_zip_path) - - def _create_supervisor_layer(self) -> None: - logger.info(f"Creating '{self.layer_name}' layer.") - self._create_layer() - logger.info(f"'{self.layer_name}' layer created.") - - def _update_supervisor_layer(self) -> None: - logger.info(f"Updating '{self.layer_name}' layer.") - self._create_layer() - logger.info(f"'{self.layer_name}' layer updated.") - - def get_latest_supervisor_layer_arn(self) -> str: - """Returns the ARN of the latest supervisor layer.""" - layer_info = self.layer.get_latest_layer_info(self.layer_name) - return layer_info.get('LayerVersionArn', "") - - def check_faas_supervisor_layer(self): - """Checks if the supervisor layer exists, if not, creates the layer. - If the layer exists and it's not updated, updates the layer.""" - # Get the layer information - layer_info = self.layer.get_latest_layer_info(self.layer_name) - # Compare supervisor versions - if layer_info and 'Description' in layer_info: - # If the supervisor layer version is lower than the passed version, - # we must update the layer - if StrUtils.compare_versions(layer_info.get('Description', ''), - self.supervisor_version) < 0: - self._update_supervisor_layer() - else: - logger.info(f"Using existent '{self.layer_name}' layer") - else: - # Layer not found, we have to create it - self._create_supervisor_layer() + return {'LayerName': self.layer_name, + 'Description': self.supervisor_version, + 'Content': {'ZipFile': FileUtils.read_file(layer_zip_path, mode="rb")}, + 'CompatibleRuntimes': ['python3.8', 'python3.7'], + 'LicenseInfo': self.resources_info.get('lambda').get('supervisor').get('license_info')} + + def _create_layer(self) -> str: + # Create tmp folders + tmp_path = FileUtils.create_tmp_dir() + layer_code_path = FileUtils.create_tmp_dir() + # Extract 'extra' and 'faassupervisor' from supervisor_zip_path + with zipfile.ZipFile(self.supervisor_zip_path) as thezip: + for file in thezip.namelist(): + # Remove the parent folder path + parent_folder, file_name = file.split('/', 1) + if file_name.startswith('extra') or file_name.startswith('faassupervisor'): + thezip.extract(file, tmp_path.name) + # Extract content of 'extra' files in layer_code_path + extra_folder_path = FileUtils.join_paths(tmp_path.name, parent_folder, 'extra') + files = FileUtils.get_all_files_in_directory(extra_folder_path) + for file_path in files: + FileUtils.unzip_folder(file_path, layer_code_path.name) + # Copy 'faassupervisor' to layer_code_path + supervisor_folder_path = FileUtils.join_paths(tmp_path.name, parent_folder, 'faassupervisor') + shutil.move(supervisor_folder_path, FileUtils.join_paths(layer_code_path.name, 'python', 'faassupervisor')) + # Create layer zip with content of layer_code_path + layer_zip_path = FileUtils.join_paths(tmp_path.name, f'{self.layer_name}.zip') + FileUtils.zip_folder(layer_zip_path, layer_code_path.name) + # Register the layer + props = self._get_supervisor_layer_props(layer_zip_path) + response = self.layer.create(**props) + return response['LayerVersionArn'] + + def _is_supervisor_created(self) -> bool: + return self.layer.exists(self.layer_name) + + def _is_supervisor_version_created(self) -> str: + versions = self.layer.list_versions(self.layer_name) + for version in versions: + if 'Description' in version: + if version['Description'] == self.supervisor_version: + return version['LayerVersionArn'] + return '' + + def get_supervisor_layer_arn(self) -> str: + """Returns the ARN of the specified supervisor layer version. + If the layer or version doesn't exists, creates the layer.""" + if self._is_supervisor_created(): + is_created = self._is_supervisor_version_created() + if is_created != '': + logger.info(f'Using existent \'{self.layer_name}\' layer.') + return is_created + logger.info((f'Creating lambda layer with \'{self.layer_name}\'' + f' version \'{self.supervisor_version}\'.')) + return self._create_layer() diff --git a/scar/utils.py b/scar/utils.py index 9ed352d3..ccb41402 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -485,7 +485,7 @@ def check_supervisor_version(cls, supervisor_version: str) -> str: return version latest_version = SupervisorUtils.get_latest_release() logger.info(('Defined supervisor version does not exists. ' - f'Using latest release: {latest_version}.')) + f'Using latest release: \'{latest_version}\'.')) return latest_version @classmethod From 22ba986e75951360264bd0ff1fdbb70ef433fcc1 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Tue, 10 Dec 2019 13:55:21 +0100 Subject: [PATCH 25/41] fixup! Unify supervisor version management --- scar/providers/aws/clients/lambdafunction.py | 2 +- scar/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scar/providers/aws/clients/lambdafunction.py b/scar/providers/aws/clients/lambdafunction.py index 62383106..c79e010b 100644 --- a/scar/providers/aws/clients/lambdafunction.py +++ b/scar/providers/aws/clients/lambdafunction.py @@ -103,7 +103,7 @@ def list_layer_versions(self, layer_name: str, next_token: Optional[str]=None) - kwargs = {'LayerName': layer_name} if next_token: kwargs['Marker'] = next_token - layer_versions_info = self.client.list_layer_version(**kwargs) + layer_versions_info = self.client.list_layer_versions(**kwargs) if 'LayerVersions' in layer_versions_info and layer_versions_info['LayerVersions']: versions.extend(layer_versions_info['LayerVersions']) if 'NextMarker' in layer_versions_info: diff --git a/scar/utils.py b/scar/utils.py index ccb41402..82243663 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -482,7 +482,7 @@ def check_supervisor_version(cls, supervisor_version: str) -> str: if GitHubUtils.exists_release_in_repo(cls._SUPERVISOR_GITHUB_USER, cls._SUPERVISOR_GITHUB_REPO, supervisor_version): - return version + return supervisor_version latest_version = SupervisorUtils.get_latest_release() logger.info(('Defined supervisor version does not exists. ' f'Using latest release: \'{latest_version}\'.')) From b391987c8537ae37fca81a48fa8661e12ffdd1a7 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Tue, 10 Dec 2019 16:12:33 +0100 Subject: [PATCH 26/41] Fix setting input bucket notifications --- scar/providers/aws/controller.py | 4 ++-- scar/providers/aws/s3.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 2e68b8e2..8cd1e906 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -274,9 +274,9 @@ def _create_s3_buckets(self, resources_info: Dict) -> None: s3_service = S3(resources_info) for bucket in resources_info.get('lambda').get('input'): if bucket.get('storage_provider') == 's3': - bucket_name, _ = s3_service.create_bucket_and_folders(bucket.get('path')) + bucket_name, folders = s3_service.create_bucket_and_folders(bucket.get('path')) Lambda(resources_info).link_function_and_bucket(bucket_name) - s3_service.set_input_bucket_notification(bucket_name) + s3_service.set_input_bucket_notification(bucket_name, folders) if resources_info.get('lambda').get('output', False): s3_service = S3(resources_info) diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index b3a0d7db..9c91fcd3 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -53,10 +53,10 @@ def create_bucket_and_folders(self, storage_path: str) -> Tuple: self.add_bucket_folder(bucket, folders) return bucket, folders - def set_input_bucket_notification(self, bucket_name: str) -> None: + def set_input_bucket_notification(self, bucket_name: str, folders: str) -> None: # First check that the function doesn't have other configurations bucket_conf = self.client.get_notification_configuration(bucket_name) - trigger_conf = self.get_trigger_configuration(bucket_name) + trigger_conf = self.get_trigger_configuration(folders) lambda_conf = [trigger_conf] if "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] @@ -69,15 +69,16 @@ def delete_bucket_notification(self, bucket_name): if bucket_conf and "LambdaFunctionConfigurations" in bucket_conf: lambda_conf = bucket_conf["LambdaFunctionConfigurations"] filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.resources_info.get('lambda').get('arn')] - notification = { "LambdaFunctionConfigurations": filter_conf } + notification = {"LambdaFunctionConfigurations": filter_conf} self.client.put_notification_configuration(bucket_name, notification) logger.info("Bucket notifications successfully deleted") - def get_trigger_configuration(self, bucket_name: str) -> Dict: - return {"LambdaFunctionArn": self.resources_info.get('lambda').get('arn'), - "Events": [ "s3:ObjectCreated:*" ], - "Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": bucket_name }]}} - } + def get_trigger_configuration(self, folders: str) -> Dict: + conf = {"LambdaFunctionArn": self.resources_info.get('lambda').get('arn'), + "Events": ["s3:ObjectCreated:*"]} + if folders != '': + conf['Filter'] = {"Key": {"FilterRules": [{"Name": "prefix", "Value": f'{folders}/'}]}} + return conf def get_file_key(self, folder_name=None, file_path=None, file_key=None): if file_key: @@ -92,8 +93,8 @@ def get_file_key(self, folder_name=None, file_path=None, file_key=None): return file_key @excp.exception(logger) - def upload_file(self, bucket: str, folder_name: str =None, file_path: str =None, file_key: str =None) -> None: - kwargs = {'Bucket' : bucket} + def upload_file(self, bucket: str, folder_name: str = None, file_path: str = None, file_key: str = None) -> None: + kwargs = {'Bucket': bucket} kwargs['Key'] = self.get_file_key(folder_name, file_path, file_key) if file_path: try: From 7927a2d7932473976a0e50e84ed39d795c11faf6 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Wed, 11 Dec 2019 12:08:33 +0100 Subject: [PATCH 27/41] Fix deletion of input bucket notifications --- scar/providers/aws/clients/lambdafunction.py | 8 ++++++++ scar/providers/aws/controller.py | 6 +++++- scar/providers/aws/lambdafunction.py | 18 ++++++++++++++++-- scar/providers/aws/s3.py | 8 ++++---- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/scar/providers/aws/clients/lambdafunction.py b/scar/providers/aws/clients/lambdafunction.py index c79e010b..6996541a 100644 --- a/scar/providers/aws/clients/lambdafunction.py +++ b/scar/providers/aws/clients/lambdafunction.py @@ -42,6 +42,14 @@ def get_function_configuration(self, function_name_or_arn: str) -> Dict: function_info['SupervisorVersion'] = self.get_supervisor_version(function_info) return function_info + def get_function(self, function_name_or_arn: str) -> Dict: + """Returns the information of the Lambda function with a link to + download the deployment package that's valid for 10 minutes.""" + function_info = self.client.get_function(FunctionName=function_name_or_arn) + # Add supervisor version + function_info['SupervisorVersion'] = self.get_supervisor_version(function_info) + return function_info + @excp.exception(logger) def get_supervisor_version(self, function_info): version = '-' diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 8cd1e906..7a22e310 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -329,7 +329,11 @@ def _delete_logs(self, resources_info: Dict): self.scar_info.get('cli_output')) def _delete_bucket_notifications(self, resources_info: Dict) -> None: - if resources_info.get('lambda').get('input', False): + lambda_client = Lambda(resources_info) + function_name = resources_info.get('lambda').get('name') + resources_info['lambda']['arn'] = lambda_client.get_function_configuration(function_name).get('FunctionArn') + resources_info['lambda']['input'] = lambda_client.get_fdl_config(function_name).get('input', False) + if resources_info.get('lambda').get('input'): for input_storage in resources_info.get('lambda').get('input'): if input_storage.get('storage_provider') == 's3': bucket_name = input_storage.get('path').split("/", 1)[0] diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index ef6640e3..ee070817 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -14,10 +14,13 @@ import base64 import json +import io +import yaml from typing import Dict from multiprocessing.pool import ThreadPool +from zipfile import ZipFile from botocore.exceptions import ClientError -from scar.http.request import call_http_endpoint +from scar.http.request import call_http_endpoint, get_file from scar.providers.aws import GenericClient from scar.providers.aws.functioncode import FunctionPackager from scar.providers.aws.lambdalayers import LambdaLayers @@ -28,6 +31,7 @@ from scar.utils import DataTypesUtils, FileUtils, StrUtils, SupervisorUtils from scar.parser.cfgfile import ConfigFileParser + MAX_CONCURRENT_INVOCATIONS = 500 ASYNCHRONOUS_CALL = {"invocation_type": "Event", "log_type": "None", @@ -200,10 +204,20 @@ def get_all_functions(self, arn_list): except ClientError as cerr: print(f"Error getting function info by arn: {cerr}") - def get_function_configuration(self, arn: str=None) -> Dict: + def get_function_configuration(self, arn: str = None) -> Dict: function = arn if arn else self.function.get('name') return self.client.get_function_configuration(function) + def get_fdl_config(self, arn: str = None) -> Dict: + function = arn if arn else self.function.get('name') + function_info = self.client.get_function(function) + dep_pack_url = function_info.get('Code').get('Location') + dep_pack = get_file(dep_pack_url) + # Extract function_config.yaml + with ZipFile(io.BytesIO(dep_pack)) as thezip: + with thezip.open('function_config.yaml') as cfg_yaml: + return yaml.safe_load(cfg_yaml) + @excp.exception(logger) def find_function(self, function_name_or_arn=None): try: diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index 9c91fcd3..efaad52b 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -71,7 +71,7 @@ def delete_bucket_notification(self, bucket_name): filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.resources_info.get('lambda').get('arn')] notification = {"LambdaFunctionConfigurations": filter_conf} self.client.put_notification_configuration(bucket_name, notification) - logger.info("Bucket notifications successfully deleted") + logger.info("Bucket notifications successfully deleted.") def get_trigger_configuration(self, folders: str) -> Dict: conf = {"LambdaFunctionArn": self.resources_info.get('lambda').get('arn'), @@ -102,9 +102,9 @@ def upload_file(self, bucket: str, folder_name: str = None, file_path: str = Non except FileNotFoundError: raise excp.UploadFileNotFoundError(file_path=file_path) if folder_name and not file_path: - logger.info(f"Folder '{kwargs['Key']}' created in bucket '{kwargs['Bucket']}'") + logger.info(f"Folder '{kwargs['Key']}' created in bucket '{kwargs['Bucket']}'.") else: - logger.info(f"Uploading file '{file_path}' to bucket '{kwargs['Bucket']}' with key '{kwargs['Key']}'") + logger.info(f"Uploading file '{file_path}' to bucket '{kwargs['Bucket']}' with key '{kwargs['Key']}'.") self.client.upload_file(**kwargs) @excp.exception(logger) @@ -142,7 +142,7 @@ def get_s3_event_list(self, bucket_name, file_keys): def download_file(self, bucket_name, file_key, file_path): kwargs = {'Bucket' : bucket_name, 'Key' : file_key} - logger.info(f"Downloading file '{file_key}' from bucket '{bucket_name}' in path '{file_path}'") + logger.info(f"Downloading file '{file_key}' from bucket '{bucket_name}' in path '{file_path}'.") with open(file_path, 'wb') as file: kwargs['Fileobj'] = file self.client.download_file(**kwargs) From e7b4bd661f0a78adf334c2746efbedb98df7193d Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Wed, 11 Dec 2019 13:09:53 +0100 Subject: [PATCH 28/41] Fix getting 'IMAGE_ID' in ls command --- scar/providers/aws/functioncode.py | 2 +- scar/providers/aws/lambdafunction.py | 17 ++++++++++++----- scar/providers/aws/response.py | 13 ++++++++----- test/functional/aws.py | 6 +++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index d2f4b2ee..c974ca22 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -99,7 +99,7 @@ def _zip_scar_folder(self, lambda_payload_path: str) -> None: save it in the expected path of the payload.""" FileUtils.zip_folder(lambda_payload_path, self.tmp_payload_folder.name, - "Creating function package") + "Creating function package.") def _check_code_size(self): # Check if the code size fits within the AWS limits diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index ee070817..7584bd9e 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -15,10 +15,10 @@ import base64 import json import io -import yaml from typing import Dict from multiprocessing.pool import ThreadPool -from zipfile import ZipFile +from zipfile import ZipFile, BadZipfile +import yaml from botocore.exceptions import ClientError from scar.http.request import call_http_endpoint, get_file from scar.providers.aws import GenericClient @@ -188,8 +188,12 @@ def _get_function_environment_variables(self): def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict: result = ConfigFileParser().get_properties().get('aws') + fdl_config = self.get_fdl_config(aws_conf.get('FunctionArn')) result['lambda']['name'] = aws_conf['FunctionName'] result['lambda']['arn'] = aws_conf['FunctionArn'] + result['lambda']['container'] = fdl_config.get('container') + result['lambda']['input'] = fdl_config.get('input') + result['lambda']['output'] = fdl_config.get('output') result['lambda']['timeout'] = aws_conf['Timeout'] result['lambda']['memory'] = aws_conf['MemorySize'] result['lambda']['environment']['Variables'] = aws_conf['Environment']['Variables'].copy() @@ -214,9 +218,12 @@ def get_fdl_config(self, arn: str = None) -> Dict: dep_pack_url = function_info.get('Code').get('Location') dep_pack = get_file(dep_pack_url) # Extract function_config.yaml - with ZipFile(io.BytesIO(dep_pack)) as thezip: - with thezip.open('function_config.yaml') as cfg_yaml: - return yaml.safe_load(cfg_yaml) + try: + with ZipFile(io.BytesIO(dep_pack)) as thezip: + with thezip.open('function_config.yaml') as cfg_yaml: + return yaml.safe_load(cfg_yaml) + except (KeyError, BadZipfile): + return {} @excp.exception(logger) def find_function(self, function_name_or_arn=None): diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index 8edd9954..3af117ef 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -124,7 +124,7 @@ def parse_ls_response(aws_resources: Dict, output_type: int) -> None: for resources_info in aws_resources: result.append(_parse_lambda_function_info(resources_info)) text_message = _get_table(result) - json_message = { aws_output : result } + json_message = {aws_output: result} _print_generic_response('', output_type, aws_output, text_message, json_output=json_message, verbose_output=json_message) @@ -134,10 +134,13 @@ def _parse_lambda_function_info(resources_info: Dict) -> Dict: stage_name = resources_info.get('api_gateway').get('stage_name') region = resources_info.get('api_gateway').get('region') api_gateway = f"https://{api_gateway}.execute-api.{region}.amazonaws.com/{stage_name}/launch" - return {'Name' : resources_info.get('lambda').get('name', "-"), - 'Memory' : resources_info.get('lambda').get('memory', "-"), - 'Timeout' : resources_info.get('lambda').get('timeout', "-"), - 'Image_id': resources_info.get('lambda').get('image', "-"), + image_id = '-' + if resources_info.get('lambda').get('container'): + image_id = resources_info.get('lambda').get('container').get('image', '-') + return {'Name': resources_info.get('lambda').get('name', "-"), + 'Memory': resources_info.get('lambda').get('memory', "-"), + 'Timeout': resources_info.get('lambda').get('timeout', "-"), + 'Image_id': image_id, 'Api_gateway': api_gateway, 'Sup_version': resources_info.get('lambda').get('supervisor').get('version', '-')} diff --git a/test/functional/aws.py b/test/functional/aws.py index 2ed62191..4688974e 100644 --- a/test/functional/aws.py +++ b/test/functional/aws.py @@ -35,9 +35,9 @@ def create_function(self, function_name): cmd = self.get_cmd(["init","-n", function_name, "-i", "centos:7"]) cmd_out = self.execute_command(cmd) self.assertTrue("Packing udocker files" in cmd_out) - self.assertTrue("Creating function package" in cmd_out) - self.assertTrue("Function '{0}' successfully created".format(function_name) in cmd_out) - self.assertTrue("Log group '/aws/lambda/{0}' successfully created".format(function_name) in cmd_out) + self.assertTrue("Creating function package." in cmd_out) + self.assertTrue("Function '{0}' successfully created.".format(function_name) in cmd_out) + self.assertTrue("Log group '/aws/lambda/{0}' successfully created.".format(function_name) in cmd_out) def test_empty_ls_table(self): cmd = self.get_cmd(["ls"]) From 84bcc762605092087d037bc014fbcd5dd4021433 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Wed, 11 Dec 2019 15:48:24 +0100 Subject: [PATCH 29/41] Download supervisor only in create_function method --- scar/providers/aws/batchfunction.py | 6 +++--- scar/providers/aws/lambdafunction.py | 32 ++++++++++++++++------------ scar/utils.py | 12 +++++++---- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/scar/providers/aws/batchfunction.py b/scar/providers/aws/batchfunction.py index de3111e1..f9caaf1c 100644 --- a/scar/providers/aws/batchfunction.py +++ b/scar/providers/aws/batchfunction.py @@ -66,7 +66,7 @@ def _delete_job_definitions(self) -> None: for job_def in _get_job_definitions(job_info): kwars = {"jobDefinition": job_def} self.client.deregister_job_definition(**kwars) - logger.info("Job definitions deleted") + logger.info("Job definitions successfully deleted.") def _get_job_queue_info(self): job_queue_info_args = {'jobQueues': [self.function_name]} @@ -88,8 +88,8 @@ def _delete_valid_job_queue(self, state): self.client.update_job_queue(**updating_args) elif state == "DISABLED": deleting_args = {'jobQueue': self.function_name} - logger.info("Job queue deleted") self.client.delete_job_queue(**deleting_args) + logger.info("Job queue successfully deleted.") def _get_describe_compute_env_args(self): return {'computeEnvironments': [self.function_name]} @@ -114,8 +114,8 @@ def _delete_valid_compute_environment(self, state): self.client.update_compute_environment(**update_args) elif state == "DISABLED": delete_args = {'computeEnvironment': self.function_name} - logger.info("Compute environment deleted") self.client.delete_compute_environment(**delete_args) + logger.info("Compute environment successfully deleted.") def _get_compute_env_args(self): account_id = self.resources_info.get('iam').get('account_id') diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 7584bd9e..d1175c59 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -47,19 +47,14 @@ def __init__(self, resources_info: Dict) -> None: super().__init__(resources_info.get('lambda', {})) self.resources_info = resources_info self.function = resources_info.get('lambda', {}) - supervisor_version = resources_info.get('lambda').get('supervisor').get('version') - self.supervisor_path = FileUtils.create_tmp_dir() - self.supervisor_zip_path = SupervisorUtils.download_supervisor( - supervisor_version, - self.supervisor_path.name - ) + self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version') - def _get_creations_args(self, zip_payload_path: str) -> Dict: + def _get_creations_args(self, zip_payload_path: str, supervisor_zip_path: str) -> Dict: return {'FunctionName': self.function.get('name'), 'Runtime': self.function.get('runtime'), 'Role': self.resources_info.get('iam').get('role'), 'Handler': self.function.get('handler'), - 'Code': self._get_function_code(zip_payload_path), + 'Code': self._get_function_code(zip_payload_path, supervisor_zip_path), 'Environment': self.function.get('environment'), 'Description': self.function.get('description'), 'Timeout': self.function.get('timeout'), @@ -76,24 +71,33 @@ def get_access_key(self) -> str: @excp.exception(logger) def create_function(self): + # Create tmp folders + supervisor_path = FileUtils.create_tmp_dir() tmp_folder = FileUtils.create_tmp_dir() + # Download supervisor + supervisor_zip_path = SupervisorUtils.download_supervisor( + self.supervisor_version, + supervisor_path.name + ) + # Manage supervisor layer + self._manage_supervisor_layer(supervisor_zip_path) + # Create function zip_payload_path = FileUtils.join_paths(tmp_folder.name, 'function.zip') - self._manage_supervisor_layer() - creation_args = self._get_creations_args(zip_payload_path) + creation_args = self._get_creations_args(zip_payload_path, supervisor_zip_path) response = self.client.create_function(**creation_args) if response and "FunctionArn" in response: self.function['arn'] = response.get('FunctionArn', "") return response - def _manage_supervisor_layer(self): - layers_client = LambdaLayers(self.resources_info, self.client, self.supervisor_zip_path) + def _manage_supervisor_layer(self, supervisor_zip_path: str) -> None: + layers_client = LambdaLayers(self.resources_info, self.client, supervisor_zip_path) self.function.get('layers', []).append(layers_client.get_supervisor_layer_arn()) @excp.exception(logger) - def _get_function_code(self, zip_payload_path: str) -> Dict: + def _get_function_code(self, zip_payload_path: str, supervisor_zip_path: str) -> Dict: '''Zip all the files and folders needed.''' code = {} - FunctionPackager(self.resources_info, self.supervisor_zip_path).create_zip(zip_payload_path) + FunctionPackager(self.resources_info, supervisor_zip_path).create_zip(zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), diff --git a/scar/utils.py b/scar/utils.py index 82243663..bbf20139 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -23,12 +23,12 @@ import tempfile import uuid import sys -import yaml from zipfile import ZipFile from io import BytesIO from typing import Optional, Dict, List, Generator, Union, Any from distutils import dir_util from packaging import version +import yaml import scar.logger as logger import scar.http.request as request from scar.exceptions import GitHubTagNotFoundError, YamlFileNotFoundError @@ -413,7 +413,10 @@ def get_latest_release(user: str, project: str) -> str: def exists_release_in_repo(user: str, project: str, tag_name: str) -> bool: """Check if a tagged release exists in a repository.""" url = f'https://api.github.com/repos/{user}/{project}/releases/tags/{tag_name}' - response = json.loads(request.get_file(url)) + response = request.get_file(url) + if not response: + return False + response = json.loads(response) if 'message' in response and response['message'] == 'Not Found': return False return True @@ -484,8 +487,9 @@ def check_supervisor_version(cls, supervisor_version: str) -> str: supervisor_version): return supervisor_version latest_version = SupervisorUtils.get_latest_release() - logger.info(('Defined supervisor version does not exists. ' - f'Using latest release: \'{latest_version}\'.')) + if supervisor_version != 'latest': + logger.info('Defined supervisor version does not exists.') + logger.info(f'Using latest supervisor release: \'{latest_version}\'.') return latest_version @classmethod From d085bb2b2200629531138440022c8af18e514576 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Thu, 12 Dec 2019 13:57:26 +0100 Subject: [PATCH 30/41] Fix creation of multiple functions --- scar/providers/aws/controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 7a22e310..3fd886c1 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -15,6 +15,7 @@ import os from typing import Dict +from copy import deepcopy from scar.cmdtemplate import Commands from scar.providers.aws.apigateway import APIGateway from scar.providers.aws.batchfunction import Batch @@ -144,6 +145,7 @@ def __init__(self, func_call): @excp.exception(logger) def init(self) -> None: for resources_info in self.aws_resources: + resources_info = deepcopy(resources_info) _check_function_defined(resources_info) # We have to create the gateway before creating the function self._create_api_gateway(resources_info) From 5d16533a7e0ddbbd079b90a6876c41836ad70f84 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Fri, 13 Dec 2019 12:09:49 +0100 Subject: [PATCH 31/41] Fix '_choose_function' func --- scar/providers/aws/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 3fd886c1..54bb1c50 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -55,7 +55,7 @@ def _choose_function(aws_resources: Dict) -> int: for idx, element in enumerate(function_names): print(f"{idx+1}) {element}") i = input("Enter number: ") - if 0 < int(i) <= len(function_names): + if 0 <= int(i) <= len(function_names): return int(i) - 1 return None @@ -66,7 +66,7 @@ def _get_all_functions(resources_info: Dict): def _check_preheat_function(resources_info: Dict): if resources_info.get('lambda').get('preheat', False): - Lambda(resources_info).preheat_function() + Lambda(resources_info).preheat_function() ############################################ ### ADD EXTRA PROPERTIES ### From 414a1215e6e39e5f999008a31fd24e2f07efd29e Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 16 Dec 2019 17:10:38 +0100 Subject: [PATCH 32/41] Update scar examples to comply with the new fdl --- examples/aws-cli/README.md | 10 ++-- examples/aws-cli/scar-aws-cli.yaml | 14 ++++-- examples/cowsay/README.md | 2 +- examples/cowsay/scar-cowsay.yaml | 7 ++- examples/cowsay/scar-minicow.yaml | 7 ++- examples/darknet/README.md | 43 +++++------------ examples/darknet/scar-darknet-api-bin.yaml | 7 --- examples/darknet/scar-darknet-api-s3.yaml | 18 ++++--- examples/darknet/scar-darknet.yaml | 20 +++++--- examples/darknet/yolo-bin.sh | 10 ---- examples/darknet/yolo.sh | 7 +-- examples/elixir/scar-elixir.yaml | 7 ++- examples/erlang/scar-erlang.yaml | 7 ++- examples/ffmpeg/README.md | 25 ++++++---- examples/ffmpeg/scar-ffmpeg.yaml | 19 +++++--- examples/imagemagick/README.md | 8 ++-- examples/imagemagick/scar-imagemagick.yaml | 17 +++++-- examples/mrbayes/README.md | 30 ++++-------- examples/mrbayes/mrbayes-sample-run.sh | 2 +- examples/mrbayes/scar-mrbayes-batch.yaml | 10 ---- .../mrbayes/scar-mrbayes-lambda-batch.yaml | 12 ----- examples/mrbayes/scar-mrbayes.yaml | 17 +++++-- examples/plant-classification/README.md | 31 +++++++++--- .../plant-classification/bootstrap-plants.sh | 2 +- .../scar-plant-classification.yaml | 21 ++++++--- examples/r/scar-r.yaml | 7 ++- examples/ruby/scar-ruby.yaml | 7 ++- examples/spectacle/README.md | 6 +-- examples/spectacle/scar-spectacle.yaml | 19 +++++--- examples/theano/scar-theano.yaml | 7 ++- examples/video-process/README.md | 47 ++++++++----------- .../scar-batch-ffmpeg-split.yaml | 8 ---- .../video-process/scar-lambda-darknet.yaml | 8 ---- .../video-process/scar-video-process.yaml | 26 ++++++++++ scar/providers/aws/controller.py | 6 ++- scar/providers/aws/functioncode.py | 9 ++-- scar/providers/aws/udocker.py | 34 ++------------ scar/utils.py | 16 ++----- scar/version.py | 2 +- 39 files changed, 276 insertions(+), 279 deletions(-) delete mode 100644 examples/darknet/scar-darknet-api-bin.yaml delete mode 100644 examples/darknet/yolo-bin.sh delete mode 100644 examples/mrbayes/scar-mrbayes-batch.yaml delete mode 100644 examples/mrbayes/scar-mrbayes-lambda-batch.yaml delete mode 100644 examples/video-process/scar-batch-ffmpeg-split.yaml delete mode 100644 examples/video-process/scar-lambda-darknet.yaml create mode 100644 examples/video-process/scar-video-process.yaml diff --git a/examples/aws-cli/README.md b/examples/aws-cli/README.md index d5e46d88..84991425 100644 --- a/examples/aws-cli/README.md +++ b/examples/aws-cli/README.md @@ -4,12 +4,12 @@ Docker image for [AWS CLI](https://aws.amazon.com/cli/) based on the [alpine](ht ## Local Usage -Credentials can be passed through the following environment variables: +Credentials can be passed to the Docker container through the following environment variables: * `AWS_ACCESS_KEY_ID` * `AWS_SECRET_ACCESS_KEY` -Assuming that these variables are already populated on your machine, you would list all the EC2 instances by issuing the command: +Assuming that these variables are already populated on your machine, you would list all your defined lambda functions by issuing the command: ```sh docker run --rm -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY grycap/aws-cli lambda list-functions @@ -27,10 +27,8 @@ You can run AWS CLI in AWS Lambda via [SCAR](https://github.com/grycap/scar) usi scar init -f scar-aws-cli.yaml ``` -2. Execute the Lambda function +2. Invoke the Lambda function with the parameters that you want to execute in the aws-cli ```sh scar run -f scar-aws-cli.yaml lambda list-functions -``` - -You have the AWS CLI running on AWS Lambda. \ No newline at end of file +``` \ No newline at end of file diff --git a/examples/aws-cli/scar-aws-cli.yaml b/examples/aws-cli/scar-aws-cli.yaml index 55950767..4de43415 100644 --- a/examples/aws-cli/scar-aws-cli.yaml +++ b/examples/aws-cli/scar-aws-cli.yaml @@ -1,6 +1,10 @@ functions: - scar-aws-cli: - image: grycap/awscli - environment: - AWS_ACCESS_KEY_ID: XXXXX - AWS_SECRET_ACCESS_KEY: XXXXX + aws: + - lambda: + name: scar-aws-cli + container: + image: grycap/awscli + environment: + Variables: + AWS_ACCESS_KEY_ID: XXXXX + AWS_SECRET_ACCESS_KEY: XXXXX diff --git a/examples/cowsay/README.md b/examples/cowsay/README.md index 0742c144..8ca69b89 100644 --- a/examples/cowsay/README.md +++ b/examples/cowsay/README.md @@ -1,6 +1,6 @@ # Alpine-less Cowsay -Docker image for [Cowsay](https://en.wikipedia.org/wiki/Cowsay) and [Fortune](https://en.wikipedia.org/wiki/Fortune_(Unix)) based on the [ubuntu:16.04](https://hub.docker.com/r/library/ubuntu/tags/16.04/) Docker image. +Docker image for [Cowsay](https://en.wikipedia.org/wiki/Cowsay) and [Fortune](https://en.wikipedia.org/wiki/Fortune_(Unix) based on the [ubuntu:16.04](https://hub.docker.com/r/library/ubuntu/tags/16.04/) Docker image. ## Local Usage diff --git a/examples/cowsay/scar-cowsay.yaml b/examples/cowsay/scar-cowsay.yaml index 2c0abbc9..c269b565 100644 --- a/examples/cowsay/scar-cowsay.yaml +++ b/examples/cowsay/scar-cowsay.yaml @@ -1,3 +1,6 @@ functions: - scar-cowsay: - image: grycap/cowsay \ No newline at end of file + aws: + - lambda: + name: scar-cowsay + container: + image: grycap/cowsay diff --git a/examples/cowsay/scar-minicow.yaml b/examples/cowsay/scar-minicow.yaml index 562b9338..2206005b 100644 --- a/examples/cowsay/scar-minicow.yaml +++ b/examples/cowsay/scar-minicow.yaml @@ -1,3 +1,6 @@ functions: - scar-minicow: - image_file: minicow.tar.gz + aws: + - lambda: + name: scar-minicow + container: + image_file: minicow.tar.gz diff --git a/examples/darknet/README.md b/examples/darknet/README.md index 67e6a7d5..ad059ac5 100644 --- a/examples/darknet/README.md +++ b/examples/darknet/README.md @@ -22,10 +22,10 @@ Create the Lambda function using the `scar-darknet.yaml` configuration file: scar init -f scar-darknet.yaml ``` -Launch the Lambda function uploading a file to the `s3://scar-darknet/scar-darknet-s3/input` folder in S3. +Launch the Lambda function uploading a file to the `s3://scar-darknet/input` folder in S3. ```sh -scar put -b scar-darknet/scar-darknet-s3/input -p dog.jpg +scar put -b scar-darknet/input -p dog.jpg ``` Take into consideration than the first invocation will take considerably longer than the subsequent ones, where the container will be cached. @@ -64,33 +64,33 @@ scar invoke -f scar-darknet-api-s3.yaml -db dog.jpg -a When the execution of the function finishes, the script used produces two output files and SCAR copies them to the S3 bucket used. To check if the files are created and copied correctly you can use the command: ```sh -scar ls -b scar-darknet/scar-darknet-api/output +scar ls -b scar-darknet/output ``` Which outputs: ``` -scar-darknet-api/output/68f5c9d5-5826-44gr-basc-8f8b23f44cdf/image-result.png -scar-darknet-api/output/68f5c9d5-5826-44gr-basc-8f8b23f44cdf/result.out +output/dog.out +output/dog.png ``` -The files are created in the output folder following the `s3://$BUCKET_NAME/$FUNCTION_NAME/output/$REQUEST_ID/*.*` structure. +The files are created in the output folder following the `s3://$BUCKET_NAME/output/*.*` structure. To download the created files you can also use SCAR. Download a folder with: ```sh -scar get -b scar-darknet/scar-darknet-api/output -p /tmp/lambda/ +scar get -b scar-darknet/output -p /tmp/lambda/ ``` -This command creates the `scar-darknet-api/ouput` folder and all the required subfolders in the `/tmp/lambda/` folder +This command creates the `ouput` folder and all the required subfolders (if any) in the `/tmp/lambda/` folder In our case the two output files are result.out: ```sh -/tmp/68f5c9d5-5826-44gr-basc-8f8b23f44cdf/input/dog.jpg: Predicted in 12.383388 seconds. -dog: 82% -truck: 64% -bicycle: 85% +/tmp/tmpzhmispbg/dog.jpg: Predicted in 28.073856 seconds. +dog: 80% +truck: 73% +bicycle: 81% ``` and image-result.png: @@ -104,21 +104,4 @@ scar rm -f scar-darknet-api-s3.yaml Have in mind that the bucket and the folders and files created are not deleted when the function is deleted. -If you want to delete the bucket you have to do it manually. - -### Processing the output locally - -Other option when invoking a synchronous function is to store the output in our machine. - -When using this option you have to make sure that the output generated by your script is the binary content that you want to save in your machine. Also due to the API Gateway limits your function has to finish in 30 seconds or less. - -The script `scar-darknet-api-bin.yaml` process the image using darknet, packages the image output and the darknet output, and then dumps the packaged file in the standard output. - -SCAR reads the output binary content and then creates the file in the specified file by the CLI command: - -```sh -scar init -f scar-darknet-api-bin.yaml -scar invoke -f scar-darknet-api-bin.yaml -db dog.jpg -o output.tar.gz -``` - -By using this functionality the user can process the function output without using S3 buckets. \ No newline at end of file +If you want to delete the bucket you have to do it manually. \ No newline at end of file diff --git a/examples/darknet/scar-darknet-api-bin.yaml b/examples/darknet/scar-darknet-api-bin.yaml deleted file mode 100644 index 2b3798a1..00000000 --- a/examples/darknet/scar-darknet-api-bin.yaml +++ /dev/null @@ -1,7 +0,0 @@ -functions: - scar-darknet-api-bin: - image: grycap/darknet - memory: 2048 - init_script: yolo-bin.sh - api_gateway: - name: darknet-bin \ No newline at end of file diff --git a/examples/darknet/scar-darknet-api-s3.yaml b/examples/darknet/scar-darknet-api-s3.yaml index 86203c22..624c6801 100644 --- a/examples/darknet/scar-darknet-api-s3.yaml +++ b/examples/darknet/scar-darknet-api-s3.yaml @@ -1,9 +1,13 @@ functions: - scar-darknet-api: - image: grycap/darknet - memory: 2048 - init_script: yolo.sh + aws: + - lambda: + name: scar-darknet-api-s3 + memory: 2048 + init_script: yolo.sh + container: + image: grycap/darknet + output: + - storage_provider: s3 + path: scar-darknet/output api_gateway: - name: darknet - s3: - input_bucket: scar-darknet \ No newline at end of file + name: darknet diff --git a/examples/darknet/scar-darknet.yaml b/examples/darknet/scar-darknet.yaml index 5ffce794..70623aea 100644 --- a/examples/darknet/scar-darknet.yaml +++ b/examples/darknet/scar-darknet.yaml @@ -1,7 +1,15 @@ functions: - scar-darknet-s3: - image: grycap/darknet - memory: 2048 - init_script: yolo.sh - s3: - input_bucket: scar-darknet + aws: + - lambda: + name: scar-darknet-s3 + memory: 2048 + init_script: yolo.sh + container: + image: grycap/darknet + input: + - storage_provider: s3 + path: scar-darknet/input + output: + - storage_provider: s3 + path: scar-darknet/output + diff --git a/examples/darknet/yolo-bin.sh b/examples/darknet/yolo-bin.sh deleted file mode 100644 index 9b741729..00000000 --- a/examples/darknet/yolo-bin.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -OUT_FOLDER="/tmp/output" -mkdir -p $OUT_FOLDER - -cd /opt/darknet -./darknet detect cfg/yolo.cfg yolo.weights $INPUT_FILE_PATH -out $OUT_FOLDER/image 2>/dev/null 1>$OUT_FOLDER/result - -tar -zcf $TMP_OUTPUT_DIR/result.tar.gz $OUT_FOLDER 2>/dev/null -cat $TMP_OUTPUT_DIR/result.tar.gz diff --git a/examples/darknet/yolo.sh b/examples/darknet/yolo.sh index 9db125ab..ac945cc2 100644 --- a/examples/darknet/yolo.sh +++ b/examples/darknet/yolo.sh @@ -1,9 +1,10 @@ #!/bin/bash -RESULT="$TMP_OUTPUT_DIR/result.out" -OUTPUT_IMAGE="$TMP_OUTPUT_DIR/image-result" +IMAGE_NAME=`basename "$INPUT_FILE_PATH" .jpg` +RESULT="$TMP_OUTPUT_DIR/$IMAGE_NAME.out" +OUTPUT_IMAGE="$TMP_OUTPUT_DIR/$IMAGE_NAME" echo "SCRIPT: Analyzing file '$INPUT_FILE_PATH', saving the result in '$RESULT' and the output image in '$OUTPUT_IMAGE.png'" cd /opt/darknet -./darknet detect cfg/yolo.cfg yolo.weights $INPUT_FILE_PATH -out $OUTPUT_IMAGE > $RESULT \ No newline at end of file +./darknet detect cfg/yolo.cfg yolo.weights $INPUT_FILE_PATH -out $OUTPUT_IMAGE > $RESULT diff --git a/examples/elixir/scar-elixir.yaml b/examples/elixir/scar-elixir.yaml index f588c491..c26bb1dc 100644 --- a/examples/elixir/scar-elixir.yaml +++ b/examples/elixir/scar-elixir.yaml @@ -1,3 +1,6 @@ functions: - scar-elixir: - image: grycap/elixir \ No newline at end of file + aws: + - lambda: + name: scar-elixir + container: + image: grycap/elixir diff --git a/examples/erlang/scar-erlang.yaml b/examples/erlang/scar-erlang.yaml index ea11d826..2159e3e6 100644 --- a/examples/erlang/scar-erlang.yaml +++ b/examples/erlang/scar-erlang.yaml @@ -1,3 +1,6 @@ functions: - scar-erlang: - image: grycap/erlang \ No newline at end of file + aws: + - lambda: + name: scar-erlang + container: + image: grycap/erlang diff --git a/examples/ffmpeg/README.md b/examples/ffmpeg/README.md index de8b5e1f..9065c982 100644 --- a/examples/ffmpeg/README.md +++ b/examples/ffmpeg/README.md @@ -16,15 +16,15 @@ In this example, the goal is that videos uploaded to an Amazon S3 bucket are aut A sample script to be executed inside the Docker container running on AWS Lambda is shown in the file [grayify-video.sh](grayify-video.sh). This script is agnostic to the Lambda function and it assumes that: -1. The user will upload the video into the `scar-ffmpeg/input` folder of an Amazon S3 bucket. -2. The input video file will automatically be made available in `tmp/$REQUEST_ID/input`, as specified by the `$INPUT_FILE_PATH` environment variable. +1. The user will upload the video into the `input` folder of the `scar-ffmpeg` Amazon S3 bucket. +2. The input video file will automatically be made available in the in the path specified by the `$INPUT_FILE_PATH` environment variable. 3. The script will convert to video to grayscale. -4. The output video file will be saved in `/tmp/$REQUEST_ID/output` that is specified by the `$TMP_OUTPUT_DIR` environment variable. -5. The video file will be automatically uploaded to the `scar-ffmpeg/output/$REQUEST_ID` folder of the Amazon S3 bucket and deleted from the underlying storage. +4. The output video file will be saved in the path specified by the `$TMP_OUTPUT_DIR` environment variable. +5. The video file will be automatically uploaded to the `output` folder of the `scar-ffmpeg` Amazon S3 bucket and deleted from the underlying storage. ## Create the Lambda function -This example assumes that the Amazon S3 bucket is `scar-test`. Since there is a flat namespace, please change this name for your tests. +This example assumes that the Amazon S3 bucket is `scar-ffmpeg`, if the bucket doesn't exist it will create it. ```sh scar init -f scar-ffmpeg.yaml @@ -33,29 +33,34 @@ scar init -f scar-ffmpeg.yaml ## Test the Lambda function Upload a video to the S3 bucket. For these examples we are using sample videos from the [ICPR 2010 Contest on Semantic Descriptio if Human Activities (SDAH 2010)](http://cvrc.ece.utexas.edu/SDHA2010/Human_Interaction.html). + ```sh -scar put -b scar-test/scar-ffmpeg/input -p seq1.avi +scar put -b scar-ffmpeg/input -p seq1.avi ``` To check the progress of the function invocation you can call the `log` command: + ```sh scar log -f scar-ffmpeg.yaml ``` -Whe the execution finishes, the converted video to grayscale will be available in `s3://scar-test/lambda-ffmpeg/output/$REQUEST_ID/seq1.avi`. Moreover you can list the files in the specified bucket with the command: +Whe the execution finishes, the converted video to grayscale will be available in `s3://scar-ffmpeg/output/seq1.avi`. Moreover you can list the files in the specified bucket with the command: + ```sh -scar ls -b scar-test/scar-ffmpeg/output/ +scar ls -b scar-ffmpeg/output/ ``` After the function finishes you can download the generated output video using the following command: + ```sh -scar get -b scar-test/scar-ffmpeg/output -p /tmp/ +scar get -b scar-ffmpeg/output -p /tmp/ ``` This command will download the ouput folder of the S3 bucket to the /tmp/ folder of your computer As an additional feature, you can also upload multiple videos to S3 using a folder instead an specific file. + ```sh -scar put -b scar-test/scar-ffmpeg/input -p /my-videos/ +scar put -b scar-ffmpeg/input -p /my-videos/ ``` Multiple concurrent Lambda invocations of the same function will process in parallel the video files. Notice that the first invocation(s) will take considerably longer until caching of the Docker container is performed. diff --git a/examples/ffmpeg/scar-ffmpeg.yaml b/examples/ffmpeg/scar-ffmpeg.yaml index aa245486..78550e0d 100644 --- a/examples/ffmpeg/scar-ffmpeg.yaml +++ b/examples/ffmpeg/scar-ffmpeg.yaml @@ -1,7 +1,14 @@ functions: - scar-ffmpeg: - image: sameersbn/ffmpeg - memory: 2048 - init_script: grayify-video.sh - s3: - input_bucket: scar-test \ No newline at end of file + aws: + - lambda: + name: scar-ffmpeg + memory: 2048 + init_script: grayify-video.sh + container: + image: sameersbn/ffmpeg + input: + - storage_provider: s3 + path: scar-ffmpeg/input + output: + - storage_provider: s3 + path: scar-ffmpeg/output diff --git a/examples/imagemagick/README.md b/examples/imagemagick/README.md index 3b83a85e..7fe65c8b 100644 --- a/examples/imagemagick/README.md +++ b/examples/imagemagick/README.md @@ -22,18 +22,18 @@ scar init -f scar-imagemagick.yaml 2. Upload a file to the S3 bucket ```sh -scar put -b scar-imagemagick/scar-imagemagick/input -p homer.png +scar put -b scar-imagemagick/input -p homer.png ``` -The converted image to grayscale will be available in `s3://scar-imagemagick/scar-imagemagick/output/$REQUEST_ID/homer.png` +The converted image to grayscale will be available in `s3://scar-imagemagick/output/homer.png` 3. Download a file from the S3 bucket ```sh -scar get -b scar-imagemagick/scar-imagemagick/output -p /tmp/ +scar get -b scar-imagemagick/output -p /tmp/ ``` -The image will be downloaded in the path `/tmp/scar-imagemagick/output/$REQUEST_ID/homer.png`. +The image will be downloaded in the path `/tmp/output/homer.png`. The first invocation will take considerable longer time than most of the subsequent invocations since the container will be cached. diff --git a/examples/imagemagick/scar-imagemagick.yaml b/examples/imagemagick/scar-imagemagick.yaml index 74c899b2..16df4083 100644 --- a/examples/imagemagick/scar-imagemagick.yaml +++ b/examples/imagemagick/scar-imagemagick.yaml @@ -1,6 +1,13 @@ functions: - scar-imagemagick: - image: grycap/imagemagick - init_script: grayify-image.sh - s3: - input_bucket: scar-imagemagick \ No newline at end of file + aws: + - lambda: + name: scar-imagemagick + init_script: grayify-image.sh + container: + image: grycap/imagemagick + input: + - storage_provider: s3 + path: scar-imagemagick/input + output: + - storage_provider: s3 + path: scar-imagemagick/output diff --git a/examples/mrbayes/README.md b/examples/mrbayes/README.md index d8127059..9a4c8a3a 100644 --- a/examples/mrbayes/README.md +++ b/examples/mrbayes/README.md @@ -2,39 +2,29 @@ Docker image for [MrBayes](http://mrbayes.sourceforge.net/) based on the [ubuntu:14.04](https://hub.docker.com/r/library/ubuntu/tags/14.04/) Docker image. -## Building docker image - -```sh -docker build -t grycap/mrbayes -f Dockerfile binary/ -``` +## Usage in AWS Lambda via SCAR -## Local Usage +You can run this image in AWS Lambda via [SCAR](https://github.com/grycap/scar) using the following procedure: -Gaining shell access: +1. Create the Lambda function ```sh -docker run --rm -ti grycap/mrbayes /bin/bash +scar init -f scar-mrbayes.yaml ``` -A sample execution can be initiated with: +2. Execute the Lambda function uploading a file to the linked bucket. ```sh -docker run --rm -ti grycap/mrbayes /tmp/mrbayes-sample-run.sh +scar put -b scar-mrbayes/input -p cynmix.nex ``` - -## Usage in AWS Lambda via SCAR - -You can run this image in AWS Lambda via [SCAR](https://github.com/grycap/scar) using the following procedure: - -1. Create the Lambda function +3. Check the function logs to see when the execution has finished. ```sh -scar init -f scar-mrbayes.yaml +scar ls -b scar-mrbayes ``` -2. Execute the Lambda function passing an execution script (in this case, specified on the configuration file) +4. Download the generated result file ```sh -scar run -f scar-mrbayes.yaml +scar get -b scar-mrbayes/output -p . ``` - diff --git a/examples/mrbayes/mrbayes-sample-run.sh b/examples/mrbayes/mrbayes-sample-run.sh index b21e34ff..e53fbaf5 100755 --- a/examples/mrbayes/mrbayes-sample-run.sh +++ b/examples/mrbayes/mrbayes-sample-run.sh @@ -26,4 +26,4 @@ mcmc file=${INPUT_FILE_PATH}3 quit EOF -mb < batch.txt +mb < batch.txt > $TMP_OUTPUT_FOLDER.out diff --git a/examples/mrbayes/scar-mrbayes-batch.yaml b/examples/mrbayes/scar-mrbayes-batch.yaml deleted file mode 100644 index 3f129c2d..00000000 --- a/examples/mrbayes/scar-mrbayes-batch.yaml +++ /dev/null @@ -1,10 +0,0 @@ -functions: - scar-mrbayes-batch: - image: grycap/mrbayes - init_script: mrbayes-sample-run.sh - execution_mode: batch - s3: - input_bucket: scar-mrbayes - environment: - ITERATIONS: "10000" - diff --git a/examples/mrbayes/scar-mrbayes-lambda-batch.yaml b/examples/mrbayes/scar-mrbayes-lambda-batch.yaml deleted file mode 100644 index a27a55ed..00000000 --- a/examples/mrbayes/scar-mrbayes-lambda-batch.yaml +++ /dev/null @@ -1,12 +0,0 @@ -functions: - scar-mrbayes-lambda-batch: - image: grycap/mrbayes - init_script: mrbayes-sample-run.sh - execution_mode: lambda-batch - time: 30 - log_level: DEBUG - environment: - ITERATIONS: "10000" - s3: - input_bucket: scar-mrbayes - \ No newline at end of file diff --git a/examples/mrbayes/scar-mrbayes.yaml b/examples/mrbayes/scar-mrbayes.yaml index 972dccab..506cfa3e 100644 --- a/examples/mrbayes/scar-mrbayes.yaml +++ b/examples/mrbayes/scar-mrbayes.yaml @@ -1,6 +1,13 @@ functions: - scar-mrbayes: - image: grycap/mrbayes - init_script: mrbayes-sample-run.sh - s3: - input_bucket: scar-mrbayes \ No newline at end of file + aws: + - lambda: + name: scar-mrbayes + init_script: mrbayes-sample-run.sh + container: + image: grycap/mrbayes + input: + - storage_provider: s3 + path: scar-mrbayes/input + output: + - storage_provider: s3 + path: scar-mrbayes/output diff --git a/examples/plant-classification/README.md b/examples/plant-classification/README.md index 3c52e884..df3fa898 100644 --- a/examples/plant-classification/README.md +++ b/examples/plant-classification/README.md @@ -20,25 +20,42 @@ In this case we will configure the Lambda function to automatically delegate to scar init -f scar-plant-classification.yaml ``` -2. Upload the processing script to Amazon S3 +2. Upload the processing script to Amazon S3. Take into account that SCAR doesn't make the files public by default, for this example to run you have to change the properties of the uploaded file and make it public. ```sh -scar put -b scar-test/scar-plants -p plant-classification-run.sh +scar put -b scar-plants -p plant-classification-run.sh ``` 3. Upload an image to the input folder of the bucket (to trigger the image processing) ```sh -scar put -b scar-test/scar-plants/input -p daisy.jpg +scar put -b scar-plants/input -p daisy.jpg ``` -If you upload several files, the corresponding jobs will be submitted to AWS Batch. Depending on the number of jobs it may decide to spawn additional EC2 instances in order to cope with the increased workload. Once the executions have finished (you can check the corresponding CloudWatch logs) ... +4. You can check the corresponding AWS Batch CloudWatch logs to see the job progress. To do this, you need to know first the request_id of the Batch job, for that you have to check the Lambda logs first and search for this output. -4. Download the generated files from the S3 bucket +```sh +scar log -f scar-plant-classification.yaml +``` +Then search for this string: + +``` +[...] +Check batch logs with: + scar log -n scar-plants -ri $BATCH_JOB_ID +[...] +``` +And finally check the job logs with the command offered: + +```sh +scar log -n scar-plants -ri $BATCH_JOB_ID +``` + +If you upload several files, the corresponding jobs will be submitted to AWS Batch. Depending on the number of jobs it may decide to spawn additional EC2 instances in order to cope with the increased workload. Once the executions have finished you can download the generated files from the S3 bucket: ```sh -scar get -b scar-test/scar-plants/output -p /tmp/plant-classification +scar get -b scar-plants/output -p /tmp/plant-classification ``` -The last command creates an `output/$REQUEST_ID` folder in the `/tmp/plant-classification` path with the files generated by each invocation. +The last command creates an `output` folder in the `/tmp/plant-classification` path with the files generated by each invocation. \ No newline at end of file diff --git a/examples/plant-classification/bootstrap-plants.sh b/examples/plant-classification/bootstrap-plants.sh index 8495c083..380a188f 100644 --- a/examples/plant-classification/bootstrap-plants.sh +++ b/examples/plant-classification/bootstrap-plants.sh @@ -1,2 +1,2 @@ #! /bin/sh -curl https://s3.amazonaws.com/scar-test/scar-plants/plant-classification-run.sh | sh - +curl https://s3.amazonaws.com/scar-plants/plant-classification-run.sh | sh - diff --git a/examples/plant-classification/scar-plant-classification.yaml b/examples/plant-classification/scar-plant-classification.yaml index 3e982084..75f34fbf 100644 --- a/examples/plant-classification/scar-plant-classification.yaml +++ b/examples/plant-classification/scar-plant-classification.yaml @@ -1,8 +1,15 @@ functions: - scar-plants: - image: deephdc/deep-oc-plant-classification-theano - memory: 1024 - execution_mode: batch - s3: - input_bucket: scar-test - init_script: bootstrap-plants.sh + aws: + - lambda: + name: scar-plants + init_script: bootstrap-plants.sh + memory: 1024 + execution_mode: batch + container: + image: deephdc/deep-oc-plant-classification-theano + input: + - storage_provider: s3 + path: scar-plants/input + output: + - storage_provider: s3 + path: scar-plants/output diff --git a/examples/r/scar-r.yaml b/examples/r/scar-r.yaml index 44563b3d..5cfc8282 100644 --- a/examples/r/scar-r.yaml +++ b/examples/r/scar-r.yaml @@ -1,3 +1,6 @@ functions: - scar-r: - image: grycap/r-base-lambda \ No newline at end of file + aws: + - lambda: + name: scar-r + container: + image: grycap/r-base-lambda diff --git a/examples/ruby/scar-ruby.yaml b/examples/ruby/scar-ruby.yaml index cdb6e1e2..708e1d72 100644 --- a/examples/ruby/scar-ruby.yaml +++ b/examples/ruby/scar-ruby.yaml @@ -1,3 +1,6 @@ functions: - scar-ruby: - image: ruby:2.2.10-slim-jessie \ No newline at end of file + aws: + - lambda: + name: scar-ruby + container: + image: ruby:2.2.10-slim-jessie diff --git a/examples/spectacle/README.md b/examples/spectacle/README.md index 4fda129e..1eae71c1 100644 --- a/examples/spectacle/README.md +++ b/examples/spectacle/README.md @@ -15,12 +15,12 @@ scar init -f scar-spectacle.yaml 2. Upload a file to the S3 bucket to launch the function ```sh -scar put -b scar-test/scar-spectacle/input -p swagger.json +scar put -b scar-spectacle/input -p swagger.json ``` 3. Download the generated files from the S3 bucket ```sh -scar get -b scar-test/scar-spectacle/output -p /tmp/spectacle +scar get -b scar-spectacle/output -p /tmp/spectacle ``` -The last command creates an `output/$REQUEST_ID` folder in the `/tmp/spectacle` path with the files generated by the spectacle invocation. \ No newline at end of file +The last command creates an `output/` folder in the `/tmp/spectacle` path with the files generated by the spectacle invocation. \ No newline at end of file diff --git a/examples/spectacle/scar-spectacle.yaml b/examples/spectacle/scar-spectacle.yaml index c533f533..da28ca30 100644 --- a/examples/spectacle/scar-spectacle.yaml +++ b/examples/spectacle/scar-spectacle.yaml @@ -1,7 +1,14 @@ functions: - scar-spectacle: - image: sourcey/spectacle - memory: 1024 - init_script: generate-documentation.sh - s3: - input_bucket: scar-test \ No newline at end of file + aws: + - lambda: + name: scar-spectacle + init_script: generate-documentation.sh + memory: 1024 + container: + image: sourcey/spectacle + input: + - storage_provider: s3 + path: scar-spectacle/input + output: + - storage_provider: s3 + path: scar-spectacle/output diff --git a/examples/theano/scar-theano.yaml b/examples/theano/scar-theano.yaml index d706dd33..37ac0263 100644 --- a/examples/theano/scar-theano.yaml +++ b/examples/theano/scar-theano.yaml @@ -1,3 +1,6 @@ functions: - scar-theano: - image: grycap/theano \ No newline at end of file + aws: + - lambda: + name: scar-theano + container: + image: grycap/theano diff --git a/examples/video-process/README.md b/examples/video-process/README.md index 69e4b5a3..8ac99935 100644 --- a/examples/video-process/README.md +++ b/examples/video-process/README.md @@ -2,21 +2,16 @@ In this example we are going to process an input video by combining the benefits of the highly-scalable AWS Lambda service with the convenience of batch-based computing provided by AWS Batch. The video is going to be split in different images and then those images are going to be analyzed by a neural network. This is a clear example of a serverless workflow. -Two different Lambda functions are defined to do this work: first, a function that creates an AWS Batch job that splits the video in 1-second pictures and stores them in S3; second, a Lambda function that processes each image to perform object detection and stores the result also in Amazon S3. - -The two different configuration files can be found in this folder. The file 'scar-batch-ffmpeg-split.yaml' defines a function that creates an AWS Batch job and the file 'scar-lambda-darknet.yaml' defines a functions that analyzes the images created. +Two different Lambda functions are defined to do this work: first `scar-batch-ffmpeg-split`, a function that creates an AWS Batch job that splits the video in 1-second pictures and stores them in S3; second `scar-lambda-darknet`, a Lambda function that processes each image to perform object detection and stores the result also in Amazon S3. Both functions are defined in the configuration file `scar-video-process.yaml`. More information about the AWS Batch integration can be found in the [documentation](https://scar.readthedocs.io/en/latest/batch.html). ## Create the processing functions -To create the functions you only need to execute two commands: +To create the workflow you only need to execute one command: ```sh -scar init -f scar-batch-ffmpeg-split.yaml -``` -```sh -scar init -f scar-lambda-darknet.yaml +scar init -f scar-video-process.yaml ``` ## Launch the execution @@ -24,49 +19,47 @@ scar init -f scar-lambda-darknet.yaml In order to launch an execution you have to upload a file to the defined input bucket of the Lambda function that creates the AWS Batch job. In this case, the following command will start the execution: ```sh -scar put -b scar-ffmpeg/scar-batch-ffmpeg-split/input -p ../ffmpeg/seq1.avi +scar put -b scar-video/input -p ../ffmpeg/seq1.avi ``` ## Process the output -When the execution of the function finishes, the script used produces two output files for each Lambda invocation. SCAR copies them to the S3 bucket specified as output. To check if the files are created and copied correctly you can use the command: +When the execution of the second function finishes, the script used produces two output files for each Lambda invocation. SCAR copies them to the S3 bucket specified as output. To check if the files are created and copied correctly you can use the command: ```sh -scar ls -b scar-ffmpeg/scar-batch-ffmpeg-split/image-output +scar ls -b scar-video/output ``` Which lists the following outputs: ``` -scar-batch-ffmpeg-split/image-output/c45433a2-e8e4-11e8-8c48-ab3c38d92053/image-result.png -scar-batch-ffmpeg-split/image-output/c45433a2-e8e4-11e8-8c48-ab3c38d92053/result.out +output/001.out +output/001.png +output/002.out +output/002.png ... -scar-batch-ffmpeg-split/image-output/c46aefe9-e8e4-11e8-97ef-8342661a6503/image-result.png -scar-batch-ffmpeg-split/image-output/c46aefe9-e8e4-11e8-97ef-8342661a6503/result.out -scar-batch-ffmpeg-split/image-output/c479475e-e8e4-11e8-995c-b14a6469fc4a/image-result.png -scar-batch-ffmpeg-split/image-output/c479475e-e8e4-11e8-995c-b14a6469fc4a/result.out +output/067.out +output/067.png +output/068.out +output/068.png ``` -The files are created in the output folder following the `s3://scar-ffmpeg/scar-batch-ffmpeg-split/image-output/$REQUEST_ID/*.*` structure. +The files are created in the output folder following the `s3://scar-video/output/*.*` structure. -To download the created files you can also use SCAR with the following command: +To download the generated files you can also use SCAR with the following command: ```sh -scar get -b scar-ffmpeg/scar-batch-ffmpeg-split/image-output -p /tmp/lambda/ +scar get -b scar-video/output -p /tmp/video/ ``` -This command creates and `image-output` folder and all the subfolders in the `/tmp/lambda/` folder +This command creates the `video/output` folder in the `/tmp` path. ## Delete the Lambda functions Do not forget to delete the functions when you finish your testing: ```sh -scar rm -f scar-batch-ffmpeg-split.yaml -``` - -```sh -scar rm -f scar-lambda-darknet.yaml +scar rm -f scar-video-process.yaml ``` Have in mind that the bucket, the folders and the files created are not deleted when the function is deleted. @@ -74,5 +67,5 @@ Have in mind that the bucket, the folders and the files created are not deleted If you want to delete the bucket you have to do it manually using, for example, AWS CLI:: ```sh - aws s3 rb s3://scar-ffmpeg --force + aws s3 rb s3://scar-video --force ``` \ No newline at end of file diff --git a/examples/video-process/scar-batch-ffmpeg-split.yaml b/examples/video-process/scar-batch-ffmpeg-split.yaml deleted file mode 100644 index 8b3587e8..00000000 --- a/examples/video-process/scar-batch-ffmpeg-split.yaml +++ /dev/null @@ -1,8 +0,0 @@ -functions: - scar-batch-ffmpeg-split: - image: grycap/ffmpeg - init_script: split-video.sh - execution_mode: batch - s3: - input_bucket: scar-ffmpeg - output_bucket: scar-ffmpeg/scar-batch-ffmpeg-split/video-output diff --git a/examples/video-process/scar-lambda-darknet.yaml b/examples/video-process/scar-lambda-darknet.yaml deleted file mode 100644 index 1881672e..00000000 --- a/examples/video-process/scar-lambda-darknet.yaml +++ /dev/null @@ -1,8 +0,0 @@ -functions: - scar-lambda-darknet: - image: grycap/darknet - memory: 3008 - init_script: yolo-sample-object-detection.sh - s3: - input_bucket: scar-ffmpeg/scar-batch-ffmpeg-split/video-output - output_bucket: scar-ffmpeg/scar-batch-ffmpeg-split/image-output \ No newline at end of file diff --git a/examples/video-process/scar-video-process.yaml b/examples/video-process/scar-video-process.yaml new file mode 100644 index 00000000..21b0672c --- /dev/null +++ b/examples/video-process/scar-video-process.yaml @@ -0,0 +1,26 @@ +functions: + aws: + - lambda: + name: scar-batch-ffmpeg-split + init_script: split-video.sh + execution_mode: batch + container: + image: grycap/ffmpeg + input: + - storage_provider: s3 + path: scar-video/input + output: + - storage_provider: s3 + path: scar-video/split-images + - lambda: + name: scar-lambda-darknet + init_script: yolo-sample-object-detection.sh + memory: 3008 + container: + image: grycap/darknet + input: + - storage_provider: s3 + path: scar-video/split-images + output: + - storage_provider: s3 + path: scar-video/output diff --git a/scar/providers/aws/controller.py b/scar/providers/aws/controller.py index 54bb1c50..294cf2ed 100644 --- a/scar/providers/aws/controller.py +++ b/scar/providers/aws/controller.py @@ -279,12 +279,16 @@ def _create_s3_buckets(self, resources_info: Dict) -> None: bucket_name, folders = s3_service.create_bucket_and_folders(bucket.get('path')) Lambda(resources_info).link_function_and_bucket(bucket_name) s3_service.set_input_bucket_notification(bucket_name, folders) + if not folders: + logger.info(f'Input bucket "{bucket_name}" successfully created') if resources_info.get('lambda').get('output', False): s3_service = S3(resources_info) for bucket in resources_info.get('lambda').get('output'): if bucket.get('storage_provider') == 's3': - s3_service.create_bucket_and_folders(bucket.get('path')) + bucket_name, folders = s3_service.create_bucket_and_folders(bucket.get('path')) + if not folders: + logger.info(f'Output bucket "{bucket_name}" successfully created') @excp.exception(logger) def _add_api_gateway_permissions(self, resources_info: Dict): diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index c974ca22..0926bd37 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -27,6 +27,7 @@ def create_function_config(resources_info): function_cfg.update(resources_info.get('lambda')) return function_cfg + class FunctionPackager(): """Class to manage the deployment package creation.""" @@ -40,10 +41,10 @@ def __init__(self, resources_info: Dict, supervisor_zip_path: str): def create_zip(self, lambda_payload_path: str) -> None: """Creates the lambda function deployment package.""" self._extract_handler_code() - self._copy_function_configuration() self._manage_udocker_images() self._add_init_script() self._add_extra_payload() + self._copy_function_configuration() self._zip_scar_folder(lambda_payload_path) self._check_code_size() @@ -67,12 +68,8 @@ def _copy_function_configuration(self): FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): - if self.resources_info.get('lambda').get('deployment').get('bucket', False) and \ - self.resources_info.get('lambda').get('container').get('image', False): - Udocker(self.resources_info, self.tmp_payload_folder.name, self.supervisor_zip_path).download_udocker_image() - if self.resources_info.get('lambda').get('image_file'): + if self.resources_info.get('lambda').get('container').get('image_file', False): Udocker(self.resources_info, self.tmp_payload_folder.name, self.supervisor_zip_path).prepare_udocker_image() - del(self.resources_info['lambda']['image_file']) def _add_init_script(self) -> None: """Copy the init script defined by the user to the payload folder.""" diff --git a/scar/providers/aws/udocker.py b/scar/providers/aws/udocker.py index 6ec4329d..dbfd75d1 100644 --- a/scar/providers/aws/udocker.py +++ b/scar/providers/aws/udocker.py @@ -13,7 +13,7 @@ # limitations under the License. from zipfile import ZipFile -from scar.utils import FileUtils, SysUtils +from scar.utils import FileUtils, SysUtils, StrUtils def _extract_udocker_zip(supervisor_zip_path) -> None: @@ -29,6 +29,8 @@ def _extract_udocker_zip(supervisor_zip_path) -> None: class Udocker(): + _CONTAINER_NAME = "udocker_container" + def __init__(self, resources_info: str, tmp_payload_folder_path: str, supervisor_zip_path: str): self.resources_info = resources_info self._tmp_payload_folder_path = tmp_payload_folder_path @@ -60,39 +62,13 @@ def _set_udocker_local_registry(self): self.resources_info['lambda']['environment']['Variables']['UDOCKER_REPOS'] = '/var/task/udocker/repos/' self.resources_info['lambda']['environment']['Variables']['UDOCKER_LAYERS'] = '/var/task/udocker/layers/' - def _create_udocker_container(self): - """Check if the container fits in the limits of the deployment.""" - if self.resources_info.get('lambda').get('deployment').get('bucket', False): - self._validate_container_size(self.resources_info.get('lambda').get('deployment').get('max_s3_payload_size')) - else: - self._validate_container_size(self.resources_info.get('lambda').get('deployment').get('max_payload_size')) - - def _validate_container_size(self, max_payload_size): - if FileUtils.get_tree_size(self._udocker_dir) < (max_payload_size / 2): - ucmd = self._udocker_exec + ["create", "--name=lambda_cont", self.resources_info.get('lambda').get('container').get('image')] - SysUtils.execute_command_with_msg(ucmd, cli_msg="Creating container structure") - self.resources_info['lambda']['environment']['Variables']['UDOCKER_CONTAINERS'] = '/var/task/udocker/containers/' - - elif FileUtils.get_tree_size(self._udocker_dir) > max_payload_size: - FileUtils.delete_folder(FileUtils.join_paths(self._udocker_dir, "containers")) - - - def download_udocker_image(self): - self._save_tmp_udocker_env() - SysUtils.execute_command_with_msg(self._udocker_exec + ["pull", self.resources_info.get('lambda').get('container').get('image')], - cli_msg="Downloading container image") - self._create_udocker_container() - self._set_udocker_local_registry() - self._restore_udocker_env() def prepare_udocker_image(self): self._save_tmp_udocker_env() - image_path = FileUtils.join_paths(FileUtils.get_tmp_dir(), "udocker_image.tar.gz") - FileUtils.copy_file(self.resources_info.get('lambda').get('image_file'), image_path) - cmd_out = SysUtils.execute_command_with_msg(self._udocker_exec + ["load", "-i", image_path], + cmd_out = SysUtils.execute_command_with_msg(self._udocker_exec + ["load", "-i", + self.resources_info.get('lambda').get('container').get('image_file')], cli_msg="Loading image file") # Get the image name from the command output self.resources_info['lambda']['container']['image'] = cmd_out.split('\n')[1] - self._create_udocker_container() self._set_udocker_local_registry() self._restore_udocker_env() diff --git a/scar/utils.py b/scar/utils.py index bbf20139..c0836e5a 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -35,6 +35,7 @@ COMMANDS = ['scar-config'] + def lazy_property(func): # Skipped type hinting: https://github.com/python/mypy/issues/3157 """ A decorator that makes a property lazy-evaluated.""" @@ -79,23 +80,11 @@ def delete_environment_variable(variable: str) -> None: def execute_command_with_msg(command: List[str], cmd_wd: Optional[str]=None, cli_msg: str='') -> str: """Execute the specified command and return the result.""" - cmd_out = subprocess.check_output(command, cwd=cmd_wd).decode('utf-8') + cmd_out = subprocess.check_output(command, cwd=cmd_wd, stderr=subprocess.STDOUT).decode('utf-8') logger.debug(cmd_out) logger.info(cli_msg) return cmd_out[:-1] - @staticmethod - def get_filtered_env_vars(key_filter: str) -> Dict: - """Returns the global variables that start with the - key_filter provided and removes the filter used.""" - size = len(key_filter) - env_vars = {} - for key, val in os.environ.items(): - # Find global variables with the specified prefix - if key.startswith(key_filter): - env_vars[key[size:]] = val - return env_vars - @staticmethod def get_user_home_path() -> str: """Returns the path of the current user's home.""" @@ -332,6 +321,7 @@ def extract_zip_from_url(url: str, dest_path: str) -> None: with ZipFile(BytesIO(url)) as thezip: thezip.extractall(dest_path) + class StrUtils: """Common methods for string management.""" diff --git a/scar/version.py b/scar/version.py index 12415c36..6571f4ca 100644 --- a/scar/version.py +++ b/scar/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '3.2.4' +__version__ = '3.2.12' From 14042fda7685f4ef645dc5e2f1b0916b85757759 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 17 Dec 2019 11:56:10 +0100 Subject: [PATCH 33/41] Fix override of api invocation headers --- scar/providers/aws/lambdafunction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index d1175c59..8d200e84 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -279,12 +279,12 @@ def call_http_endpoint(self): def _set_invoke_args(self, invoke_args): if self.resources_info.get('api_gateway').get('data_binary', False): invoke_args['data'] = self._get_b64encoded_binary_data() - invoke_args['headers'] = {'Content-Type': 'application/octet-stream'} + invoke_args['headers'].update({'Content-Type': 'application/octet-stream'}) if self.resources_info.get('api_gateway').get('parameters', False): invoke_args['params'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('parameters')) if self.resources_info.get('api_gateway').get('json_data', False): invoke_args['data'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('json_data')) - invoke_args['headers'] = {'Content-Type': 'application/json'} + invoke_args['headers'].update({'Content-Type': 'application/json'}) def _parse_http_parameters(self, parameters): return parameters if type(parameters) is dict else json.loads(parameters) From 3a50f2540c24259f9ec88c03d0019566099dc0c2 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 17 Dec 2019 14:50:24 +0100 Subject: [PATCH 34/41] Fix error that create folders again whe uploading a file Additionally, now when a folder is created in S3, the 'ContentType' of the file is set to 'application/x-directory' --- scar/providers/aws/clients/s3.py | 15 +++++++++++++++ scar/providers/aws/s3.py | 10 ++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/scar/providers/aws/clients/s3.py b/scar/providers/aws/clients/s3.py index 0135f299..c0dbefbe 100644 --- a/scar/providers/aws/clients/s3.py +++ b/scar/providers/aws/clients/s3.py @@ -69,6 +69,21 @@ def download_file(self, **kwargs: Dict) -> Dict: """Download an object from S3 to a file-like object.""" return self.client.download_fileobj(**kwargs) + @exception(logger) + def is_folder(self, bucket: str, folder: str) -> bool: + """Checks if a file with the key specified exists.""" + try: + kwargs = {'Bucket' : bucket, + 'Key' : folder if folder.endswith('/') else folder + '/'} + # If this call works the folder exist + self.client.get_object(**kwargs) + return True + except ClientError as cerr: + # Folder not found + if cerr.response['Error']['Code'] == 'NoSuchKey': + return False + raise cerr + @exception(logger) def list_files(self, **kwargs: Dict) -> List: """Returns the keys of all the objects in a bucket. diff --git a/scar/providers/aws/s3.py b/scar/providers/aws/s3.py index efaad52b..75a5b0dc 100644 --- a/scar/providers/aws/s3.py +++ b/scar/providers/aws/s3.py @@ -44,7 +44,8 @@ def create_bucket(self, bucket_name) -> None: @excp.exception(logger) def add_bucket_folder(self, bucket: str, folders: str) -> None: - self.upload_file(bucket, folder_name=folders) + if not self.client.is_folder(bucket, folders): + self.upload_file(bucket, folder_name=folders) def create_bucket_and_folders(self, storage_path: str) -> Tuple: bucket, folders = get_bucket_and_folders(storage_path) @@ -93,7 +94,7 @@ def get_file_key(self, folder_name=None, file_path=None, file_key=None): return file_key @excp.exception(logger) - def upload_file(self, bucket: str, folder_name: str = None, file_path: str = None, file_key: str = None) -> None: + def upload_file(self, bucket: str, folder_name: str=None, file_path: str=None, file_key: str=None) -> None: kwargs = {'Bucket': bucket} kwargs['Key'] = self.get_file_key(folder_name, file_path, file_key) if file_path: @@ -102,13 +103,14 @@ def upload_file(self, bucket: str, folder_name: str = None, file_path: str = Non except FileNotFoundError: raise excp.UploadFileNotFoundError(file_path=file_path) if folder_name and not file_path: + kwargs['ContentType'] = 'application/x-directory' logger.info(f"Folder '{kwargs['Key']}' created in bucket '{kwargs['Bucket']}'.") else: logger.info(f"Uploading file '{file_path}' to bucket '{kwargs['Bucket']}' with key '{kwargs['Key']}'.") self.client.upload_file(**kwargs) @excp.exception(logger) - def get_bucket_file_list(self, storage: Dict = None): + def get_bucket_file_list(self, storage: Dict=None): files = [] if storage: files = self._list_storage_files(storage) @@ -117,7 +119,7 @@ def get_bucket_file_list(self, storage: Dict = None): if storage_info.get('storage_provider') == 's3': files.extend(self._list_storage_files(storage_info)) return files - + def _list_storage_files(self, storage: Dict) -> List: files = [] bucket_name, folder_path = get_bucket_and_folders(storage.get('path')) From 304589c787bc5214fc1d6cbc28e324296545e879 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Wed, 18 Dec 2019 14:16:59 +0100 Subject: [PATCH 35/41] Add example file with fdl configuration options --- fdl-example.yaml | 191 ++++++++++++++++++++++++++++++++++++++++++ scar/parser/fdl2.yaml | 46 ---------- 2 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 fdl-example.yaml delete mode 100644 scar/parser/fdl2.yaml diff --git a/fdl-example.yaml b/fdl-example.yaml new file mode 100644 index 00000000..1758cc36 --- /dev/null +++ b/fdl-example.yaml @@ -0,0 +1,191 @@ +# This file shows all the possible values that can be defined to confire the functions and their linked services +# Most of this values are already defined in the SCAR default configuration file +# The values define in a configuration file are only applied to the function or functions being deployed +# To override permanently some of this values and apply them to all the deployed functions, please edit the SCAR default configuration file in ~/.scar/scar.cfg +# ---------------------------------------------------------------------------------------------------------------- +functions: + # Define different providers under this property. Only supported 'aws' + aws: + # Define a list of functions under this property. + # You can define the function properties and all its related services + # Possible values are 'lambda', 'iam', 'api_gateway', 'cloudwatch', 'batch' + # REQUIRED 'lambda' + - lambda: + # Boto profile used for the lambda client + # Default 'default' + # Must match the profiles in the file ~/.aws/credentials + boto_profile: default + # Region of the function, can be any region supported by AWS + # Default 'us-east-1' + region: us-east-1 + # Function's name + # REQUIRED + name: function1 + # Memory of the function, in MB, min 128, max 3008. Default '512' + memory: 1024 + # Maximum execution time in seconds, max 900. Default '300' + timeout: 300 + # Set job delegation or not + # Possible values 'lambda', 'lambda-batch', 'batch' + # Default 'lambda' + execution_mode: lambda + "description": "Automatically generated lambda function", + # Supervisor log level + # Can be INFO, DEBUG, ERROR, WARNING + # Default 'INFO' + log_level: INFO + # Lambda function's layers arn (max 4). + # SCAR adds the supervisor layer automatically + layers: + - arn:.... + # Environment variables of the function + # This variables are used in the lambda's environment, not the container's environment + environment: + Variables: + KEY1: val1 + KEY2: val2 + # Script executed inside of the function's container + init_script: ffmpeg-script.sh + # Define udocker container properties + container: + # Container image to use. REQUIRED + image: jrottenberg/ffmpeg:4.1-ubuntu + # Time used to post-process data generated by the container + # This time is substracted from the total time set for the function + # If there are a lot of files to upload as output, maybe this value has to be increased + # Default '10' seconds + timeout_threshold": 10 + # Environment variables of the container + # These variables are passed to the container environment, that is, can be accessed from the user's script + environment: + Variables: + KEY1: val1 + KEY2: val2 + # Define input storage providers linked with the function + input: + # Storage type + # Possible values 'minio', 's3', 'onedata' + - storage: minio + # Complete path of the bucket with folders 'if any' + path: my-bucket/test + # Define a filter for the parsed files based on prefix or suffix + files: + # Possible values 'prefix', 'suffix' + suffix: + # List of suffixes to filter (can be any string) + - wav + - srt + # Define output storage providers linked with the function + output: + - storage: s3 + path: my-bucket/test-output + files: + prefix: + - wav + # Properties for the faas-supervisor used in the inside the lambda function + supervisor: + # Must be a Github tag or "latest". Default 'latest' + version: latest + + + # Set IAM properties + iam: + boto_profile: default + # The Amazon Resource Name (ARN) of the function's execution role. + # This value is usually set for all the functions in the SCAR's default configuration file + # REQUIRED + role: "" + + + # Set API Gateway properties + # All these values are set by default + api_gateway: + boto_profile: default + region: us-east-1 + + + # Set CloudWatch properties + # All these values are set by default + cloudwatch: + boto_profile: default + region: us-east-1 + # Number of days that the functions logs are stored + log_retention_policy_in_days: 30 + + + # Set AWS Batch properties. + # Only used when execution mode in 'lambda' is set to 'lambda-batch' or 'batch' + batch: + boto_profile: default + region: us-east-1 + # The number of vCPUs reserved for the container + # Used in the job definition + # Default 1 + vcpus: 1 + # The hard limit (in MiB) of memory to present to the container + # Used in the job definition + # Default 1024 + memory: 1024 + # Request GPU resources for the launched container + # Default 'False'. Values 'False', 'True' + enable_gpu: False + # The full arn of the IAM role that allows AWS Batch to make calls to other AWS services on your behalf. + service_role: "arn:..." + # Environment variables passed to the batch container + environment: + Variables: + KEY1: val1 + KEY2: val2 + compute_resources: + # List of the Amazon EC2 security groups associated with instances launched in the compute environment + # REQUIRED when using batch + security_group_ids: + - sg-12345678 + # The desired number of Amazon EC2 vCPUS in the compute environment + # Default 0 + desired_v_cpus: 0 + # The minimum number of Amazon EC2 vCPUs that an environment should maintain + # Default 0 + min_v_cpus: 0 + # The maximum number of Amazon EC2 vCPUs that an environment can reach + # Default 2 + max_v_cpus: 2 + # List of the VPC subnets into which the compute resources are launched. + # REQUIRED when using batch + subnets: + - subnet-12345 + subnet-67891 + # The instances types that may be launched. + # You can specify instance families to launch any instance type within those families (for example, c5 or p3 ), or you can specify specific sizes within a family (such as c5.8xlarge ). + # You can also choose optimal to pick instance types (from the C, M, and R instance families) on the fly that match the demand of your job queues. + # Default 'm3.medium' + instance_types: + - "m3.medium" + # The Amazon ECS instance profile applied to Amazon EC2 instances in a compute environment + instance_role: "arn:..." + + +# Define different storage providers connections. Supported 's3','minio', 'onedata' +# If you use a default S3 storage with the default boto configuration, this properties are not needed. +storages: + # Define S3 properties + # If used, REQUIRED properties are 'access_key', 'secret_key' + # The supervisor will try to create the boto3 client using the function permissions (in AWS Lambda environment) + s3: + access_key: awsuser + secret_key: awskey + region: us-east-1 + # Define minio properties + # If used, REQUIRED properties are 'access_key', 'secret_key' + minio: + endpoint: minio-endpoint + verify: '' + region: minio-region + access_key: muser + secret_key: mpass + # Define onedata properties + # If used, REQUIRED properties are 'oneprovider_host', 'token', 'space' + onedata: + oneprovider_host: op-host + token: mytoken + space: onedata_space diff --git a/scar/parser/fdl2.yaml b/scar/parser/fdl2.yaml deleted file mode 100644 index 058de053..00000000 --- a/scar/parser/fdl2.yaml +++ /dev/null @@ -1,46 +0,0 @@ -functions: - aws: - - lambda: - name: function1 - input: - - storage: minio - path: my-bucket/test - - storage: s3 - path: s3-bucket/test1 - output: - - storage: minio - path: my-bucket/test-output - files: - sufix: - - wav - - srt - - storage: s3 - path: s3-bucket/test1-output - files: - suffix: - - avi - - lambda: - name: function2 - execution_mode: batch - input: - - storage: minio - path: my-bucket2/test - output: - - storage: minio - path: my-bucket2/test-output - files: - prefix: # Possible values: 'prefix', 'suffix' - - my_file - -storages: - minio: # Possible values: 'minio', 's3', 'onedata' - access_key: muser # Possible values: 'access_key', 'secret_key', 'region' - secret_key: mpass - s3: - access_key: awsuser # Possible values: 'access_key', 'secret_key', 'region' (if not defined supervisor will - secret_key: awskey # try to create the boto3 client using the function permissions (in AWS Lambda environment)) - region: us-east-1 - onedata: - oneprovider_host: op-host # Possible values: 'oneprovider_host', 'token', 'space' - token: mytoken - space: onedata_space From bff2ea0fad607c9e205efbb8851f935629999463 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Thu, 19 Dec 2019 11:11:53 +0100 Subject: [PATCH 36/41] Update FDL example --- fdl-example.yaml | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/fdl-example.yaml b/fdl-example.yaml index 1758cc36..7a2dc457 100644 --- a/fdl-example.yaml +++ b/fdl-example.yaml @@ -65,23 +65,22 @@ functions: input: # Storage type # Possible values 'minio', 's3', 'onedata' - - storage: minio + - storage_provider: minio # Complete path of the bucket with folders 'if any' path: my-bucket/test - # Define a filter for the parsed files based on prefix or suffix - files: - # Possible values 'prefix', 'suffix' - suffix: - # List of suffixes to filter (can be any string) - - wav - - srt # Define output storage providers linked with the function output: - - storage: s3 + - storage_provider: s3 path: my-bucket/test-output - files: - prefix: - - wav + # Define optional filters to upload the output files based on prefix or suffix + # Possible values 'prefix', 'suffix' + suffix: + # List of suffixes to filter (can be any string) + - wav + - srt + prefix: + # List of prefixes to filter (can be any string) + - result- # Properties for the faas-supervisor used in the inside the lambda function supervisor: # Must be a Github tag or "latest". Default 'latest' @@ -167,7 +166,7 @@ functions: # Define different storage providers connections. Supported 's3','minio', 'onedata' # If you use a default S3 storage with the default boto configuration, this properties are not needed. -storages: +storage_providers: # Define S3 properties # If used, REQUIRED properties are 'access_key', 'secret_key' # The supervisor will try to create the boto3 client using the function permissions (in AWS Lambda environment) @@ -179,8 +178,8 @@ storages: # If used, REQUIRED properties are 'access_key', 'secret_key' minio: endpoint: minio-endpoint - verify: '' - region: minio-region + verify: True + region: us-east-1 access_key: muser secret_key: mpass # Define onedata properties From 2d7b9155f914ff5a9dc973631e83890896f2ee4f Mon Sep 17 00:00:00 2001 From: Alfonso Date: Thu, 19 Dec 2019 18:08:54 +0100 Subject: [PATCH 37/41] WIP - Update documentation Also fix bug when using a deployment bucket to deploy a docker image --- docs/source/advanced_usage.rst | 254 ++++++++++++++++----------- docs/source/basic_usage.rst | 18 +- docs/source/configuration.rst | 156 +++++++++++++--- docs/source/testing.rst | 16 +- scar/parser/cli/__init__.py | 3 + scar/providers/aws/functioncode.py | 4 +- scar/providers/aws/lambdafunction.py | 2 +- scar/utils.py | 2 +- 8 files changed, 295 insertions(+), 160 deletions(-) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index f087d346..ea666083 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -6,22 +6,43 @@ Define a shell-script for each invocation of the Lambda function Instead of packaging the script to be used inside the container image and having to modify the image each time you want to modify the script, you can specify a shell-script when initializing the Lambda function to trigger its execution inside the container on each invocation of the Lambda function. For example:: - cat >> init-script.yaml << EOF + cat >> cow.sh << EOF + #!/bin/bash + /usr/games/cowsay "Executing init script !!" + EOF + + cat >> cow.yaml << EOF functions: - scar-cowsay: - image: grycap/cowsay - init_script: src/test/test-cowsay.sh + aws: + - lambda: + name: scar-cowsay + init_script: cow.sh + container: + image: grycap/cowsay EOF - scar init -f init-script.yaml + scar init -f cow.yaml or using CLI parameters:: - scar init -s src/test/test-cowsay.sh -n scar-cowsay -i grycap/cowsay + scar init -s cow.sh -n scar-cowsay -i grycap/cowsay Now whenever this Lambda function is executed, the script will be run in the container:: - scar run -f init-script.yaml + scar run -f cow.yaml + + Request Id: fb925bfa-bc65-47d5-beed-077f0de471e2 + Log Group Name: /aws/lambda/scar-cowsay + Log Stream Name: 2019/12/19/[$LATEST]0eb088e8a18d4599a572b7bf9f0ed321 + __________________________ + < Executing init script !! > + -------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + As explained in next section, this can be overridden by speciying a different shell-script when running the Lambda function. @@ -31,24 +52,69 @@ Executing an user-defined shell-script You can execute the Lambda function and specify a shell-script locally available in your machine to be executed within the container:: - cat >> run-script.yaml << EOF + cat >> runcow.sh << EOF + #!/bin/bash + /usr/games/cowsay "Executing run script !!" + EOF + + cat >> cow.yaml << EOF functions: - scar-cowsay: - image: grycap/cowsay - run_script: src/test/test-cowsay.sh + aws: + - lambda: + name: scar-cowsay + run_script: runcow.sh + container: + image: grycap/cowsay EOF - scar run -f run-script.yaml + scar init -f cow.yaml -or using CLI parameters:: +Now if you execute the function without passing more parameters, the entrypoint of the container is executed:: + + scar run -n scar-cowsay + + Request Id: 97492a12-ca84-4539-be80-45696501ee4a + Log Group Name: /aws/lambda/scar-cowsay + Log Stream Name: 2019/12/19/[$LATEST]d5cc7a9db9b44e529873130f6d005fe1 + ____________________________________ + / No matter where I go, the place is \ + \ always called "here". / + ------------------------------------ + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + +But, when you use the configuration file with the ``run_script`` property:: + + scar run -f cow.yaml - scar run -s src/test/test-cowsay.sh -n scar-cowsay +or use CLI parameters:: -or a combination of both (to avoid editing the .yaml file):: + scar run -n scar-cowsay -s runcow.sh - scar run -f run-script.yaml -s /tmp/test-cowsay.sh +or a combination of both (to avoid editing the initial *.yaml file):: -Have in mind that the script used in combination with the run command is no saved anywhere. It is uploaded and executed inside the container, but the container image is not updated. The shell-script needs to be specified and can be changed in each different execution of the Lambda function. + scar run -f cow.yaml -s runcow.sh + +the passed script is executed:: + + Request Id: db3ff40e-ab51-4f90-95ad-7473751fb9c7 + Log Group Name: /aws/lambda/scar-cowsay + Log Stream Name: 2019/12/19/[$LATEST]d5cc7a9db9b44e529873130f6d005fe1 + _________________________ + < Executing run script !! > + ------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + +Have in mind that the script used in combination with the run command is no saved anywhere. +It is uploaded and executed inside the container, but the container image is not updated. +The shell-script needs to be specified and can be changed in each different execution of the Lambda function. Passing environment variables @@ -57,67 +123,46 @@ Passing environment variables You can specify environment variables to the init command which will be in turn passed to the executed Docker container and made available to your shell-script. Using a configuration file:: - cat >> env-var.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - init_script: src/test/test-global-vars.sh - environment: - TEST1: 45 - TEST2: 69 + cat >> cow.sh << EOF + #!/bin/bash + env | /usr/games/cowsay EOF - scar init -f env-var.yaml - -or using CLI parameters:: - - scar init -e TEST1=45 -e TEST2=69 -s src/test/test-global-vars.sh -n scar-cowsay - -You can also update the environment variables by changing the configuration file and then using the update command:: - - cat >> env-var.yaml << EOF + cat >> cow-env.yaml << EOF functions: - scar-cowsay: - image: grycap/cowsay - init_script: src/test/test-global-vars.sh - environment: - TEST1: 145 - TEST2: i69 - TEST3: 42 + aws: + - lambda: + name: scar-cowsay + run_script: runcow.sh + container: + image: grycap/cowsay + environment: + Variables: + TESTKEY1: val1 + TESTKEY2: val2 EOF - scar update -f env-var.yaml + scar init -f cow-env.yaml -or:: - - scar update -e EST1: 145 -e TEST2: i69 -e TEST2: 42 -n scar-cowsay - -In addition, the following environment variables are automatically made available to the underlying Docker container: - -* AWS_ACCESS_KEY_ID -* AWS_SECRET_ACCESS_KEY -* AWS_SESSION_TOKEN -* AWS_SECURITY_TOKEN - -This allows a script running in the Docker container to access other AWS services. As an example, see how the AWS CLI runs on AWS Lambda in the `examples/aws-cli `_ folder. - - -Executing cli commands ----------------------- - -To run commands inside the docker image you can specify the command to be executed at the end of the command line:: +or using CLI parameters:: - scar run -f basic-cow.yaml ls + scar init -n scar-cowsay -i grycap/cowsay -e TEST1=45 -e TEST2=69 -s cow.sh -Passing arguments -^^^^^^^^^^^^^^^^^ +Executing custom commands and arguments +--------------------------------------- -You can also supply arguments which will be passed to the command executed in the Docker container:: +To run commands inside the docker image you can specify the command to be executed at the end of the command line. +This command overrides any ``init`` or ``run`` script defined:: - scar run -f basic-cow.yaml /usr/bin/perl /usr/games/cowsay Hello World + scar run -f cow.yaml df -h -Note that since cowsay is a Perl script you will have to prepend it with the location of the Perl interpreter (in the Docker container). + Request Id: 39e6fc0d-6831-48d4-aa03-8614307cf8b7 + Log Group Name: /aws/lambda/scar-cowsay + Log Stream Name: 2019/12/19/[$LATEST]9764af5bf6854244a1c9469d8cb84484 + Filesystem Size Used Avail Use% Mounted on + /dev/root 526M 206M 309M 41% / + /dev/vdb 1.5G 21M 1.4G 2% /dev Obtaining a JSON Output @@ -125,63 +170,64 @@ Obtaining a JSON Output For easier scripting, a JSON output can be obtained by including the `-j` or the `-v` (even more verbose output) flags:: - scar run -f basic-cow.yaml -j + scar run -f cow.yaml -j -Upload docker images using an S3 bucket ---------------------------------------- - -If you want to save some space inside the lambda function you can deploy a lambda function using an S3 bucket by issuing the following command:: - - cat >> s3-bucket.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - s3: - deployment_bucket: scar-cowsay - EOF + { "LambdaOutput": + { + "StatusCode": 200, + "Payload": " _________________________________________\n/ \"I always avoid prophesying beforehand \\\n| because it is much better |\n| |\n| to prophesy after the event has already |\n| taken place. \" - Winston |\n| |\n\\ Churchill /\n -----------------------------------------\n \\ ^__^\n \\ (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||\n", + "LogGroupName": "/aws/lambda/scar-cowsay", + "LogStreamName": "2019/12/19/[$LATEST]a4ba02914fd14ab4825d6c6635a1dfd6", + "RequestId": "fcc4e24c-1fe3-4ca9-9f00-b15ec18c1676" + } + } - scar init -f s3-bucket.yaml - -or using the CLI:: - - scar init -db scar-cowsay -n scar-cowsay -i grycap/cowsay - -The maximum deployment package size allowed by AWS is an unzipped file of 250MB. With this restriction in mind, SCAR downloads the docker image to a temporal folder and creates the udocker file structure needed. -* If the image information and the container filesystem fit in the 250MB SCAR will upload everything and the lambda function will not need to download or create a container structure thus improving the execution time of the function. This option gives the user the full 500MB of ``/tmp/`` storage. -* If the container filesystem doesn't fit in the deployment package SCAR will only upload the image information, that is, the layers. Also the lambda function execution time is improved because it doesn't need to dowload the container. In this case udocker needs to create the container filesystem so the first function invocation can be delayed for a few of seconds. This option usually duplicates the available space in the ``/tmp/`` folder with respect to the SCAR standard initialization. Upload docker image files using an S3 bucket -------------------------------------------- -SCAR also allows to upload a saved docker image:: +SCAR allows to upload a saved docker image. +We created the image file with the command ``docker save grycap/cowsay > cowsay.tar.gz``:: - cat >> s3-bucket.yaml << EOF + cat >> cow.yaml << EOF functions: - scar-cowsay: - image_file: slim_cow.tar.gz - s3: - deployment_bucket: scar-cowsay + aws: + - lambda: + name: scar-cowsay + container: + image_file: cowsay.tar.gz + deployment: + bucket: scar-test EOF - scar init -f s3-bucket.yaml + scar init -f cow.yaml -and for the CLI fans:: +or for the CLI fans:: - scar init -db scar-cowsay -n scar-cowsay -if slim_cow.tar.gz + scar init -db scar-cowsay -n scar-cowsay -if cowsay.tar.gz -The behavior of SCAR is the same as in the case above (when uploading an image from docker hub). The image file is unpacked in a temporal folder and the udocker layers and container filesystem are created. Depending on the size of the layers and the filesystem, SCAR will try to upload everything or only the image layers. +Have in mind that the maximum deployment package size allowed by AWS is an unzipped file of 250MB. +The image file is unpacked in a temporal folder and the udocker layers are created. +Depending on the size of the layers, SCAR will try to upload them or will show the user an error. Upload 'slim' docker image files in the payload ----------------------------------------------- -Finally, if the image is small enough, SCAR allows to upload it in the function payload. Due to the SCAR libraries weighting ~10MB, the maximum size of the image uploaded using this method should not be bigger than ~40MB:: +Finally, if the image is small enough, SCAR allows to upload it in the function payload wich is ~50MB:: + + docker save grycap/minicow > minicow.tar.gz - cat >> slim-image.yaml << EOF + cat >> minicow.yaml << EOF functions: - scar-cowsay: - image_file: slimcow.tar.gz + aws: + - lambda: + name: scar-cowsay + container: + image_file: minicow.tar.gz EOF - scar init -f slim-image.yaml + scar init -f minicow.yaml -To help with the creation of slim images, you can use `minicon `_. Minicon is a general tool to analyze applications and executions of these applications to obtain a filesystem that contains all the dependencies that have been detected. By using minicon the size of the cowsay image was reduced from 170MB to 11MB. +To help with the creation of slim images, you can use `minicon `_. +Minicon is a general tool to analyze applications and executions of these applications to obtain a filesystem that contains all the dependencies that have been detected. +By using minicon the size of the cowsay image was reduced from 170MB to 11MB. \ No newline at end of file diff --git a/docs/source/basic_usage.rst b/docs/source/basic_usage.rst index 20c133ca..f2fa3c7c 100644 --- a/docs/source/basic_usage.rst +++ b/docs/source/basic_usage.rst @@ -8,8 +8,11 @@ Using a configuration file (recommended) cat >> basic-cow.yaml << EOF functions: - scar-cowsay: - image: grycap/cowsay + aws: + - lambda: + name: scar-cowsay + container: + image: grycap/cowsay EOF Where you define the name of the function and under it the image that will run inside the function. @@ -26,17 +29,6 @@ Using a configuration file (recommended) scar log -f basic-cow.yaml - If you want to get an specific log stream or request id from the logs you can specify it either in the configuration file or in the command line, although due to the dinamic nature of those parameters its easier to specify them in the cli. - To retrieve an specific log stream or request id using the configuration file would be:: - - cat >> basic-cow.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - log_stream_name: 2018/07/10/[$LATEST]037b5bbf77a44a5basdfwerb92805303 - request_id: bc456798-841a-11e8-8z1b-49c89abc6ff1 - EOF - 5) Finally to delete the function:: scar rm -f basic-cow.yaml diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index ad17e8d4..8a29a1d3 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -45,28 +45,134 @@ Configuration file ^^^^^^^^^^^^^^^^^^ The first time you execute SCAR a default configuration file is created in the user location: ``$HOME/.scar/scar.cfg``. -As explained above, it is mandatory to set a value for the aws.iam.role property. The rest of the values can be customized to your preferences:: - - { "aws" : { - "iam" : {"role" : ""}, - "lambda" : { - "region" : "us-east-1", - "time" : 300, - "memory" : 512, - "description" : "Automatically generated lambda function", - "timeout_threshold" : 10 }, - "cloudwatch" : { "log_retention_policy_in_days" : 30 }} - } - - -The values represent: - -* **aws.iam.role**: The `ARN `_ of the IAM Role that you just created in the previous section. -* **aws.lambda.region**: The `AWS region `_ on which the AWS Lambda function will be created. -* **aws.lambda.time**: Default maximum execution time of the AWS Lambda function [1]_. -* **aws.lambda.memory**: Default maximum memory allocated to the AWS Lambda function [1]_. -* **aws.lambda.description**: Default description of the AWS Lambda function [1]_. -* **aws.lambda.timeout_threshold:** Default time used to postprocess the container output. Also used to avoid getting timeout error in case the execution of the container takes more time than the lambda_time [1]_. -* **aws.cloudwatch.log_retention_policy_in_days**: Default time (in days) used to store the logs in cloudwatch. Any log older than this parameter will be deleted. - -.. [1] These parameters can also be set or updated with the SCAR CLI \ No newline at end of file +As explained above, it is mandatory to set a value for the ``aws.iam.role`` property to use the Lambda service. +If you also want to use the Batch service you have to update the values of the ``aws.batch.compute_resources.security_group_ids``, and ``aws.batch.compute_resources.subnets``. +An explanation of all the configurable properties can be found in the `example configuration file `_. +Below is the complete default configuration file :: + { + "scar": { + "config_version": "1.0.9" + }, + "aws": { + "iam": { + "boto_profile": "default", + "role": "" + }, + "lambda": { + "boto_profile": "default", + "region": "us-east-1", + "execution_mode": "lambda", + "timeout": 300, + "memory": 512, + "description": "Automatically generated lambda function", + "runtime": "python3.7", + "layers": [], + "invocation_type": "RequestResponse", + "asynchronous": false, + "log_type": "Tail", + "log_level": "INFO", + "environment": { + "Variables": { + "UDOCKER_BIN": "/opt/udocker/bin/", + "UDOCKER_LIB": "/opt/udocker/lib/", + "UDOCKER_DIR": "/tmp/shared/udocker", + "UDOCKER_EXEC": "/opt/udocker/udocker.py" + } + }, + "deployment": { + "max_payload_size": 52428800, + "max_s3_payload_size": 262144000 + }, + "container": { + "environment": { + "Variables": {} + }, + "timeout_threshold": 10 + }, + "supervisor": { + "version": "1.2.0-rc4", + "layer_name": "faas-supervisor", + "license_info": "Apache 2.0" + } + }, + "s3": { + "boto_profile": "default", + "region": "us-east-1", + "event": { + "Records": [ + { + "eventSource": "aws:s3", + "s3": { + "bucket": { + "name": "{bucket_name}", + "arn": "arn:aws:s3:::{bucket_name}" + }, + "object": { + "key": "{file_key}" + } + } + } + ] + } + }, + "api_gateway": { + "boto_profile": "default", + "region": "us-east-1", + "endpoint": "https://{api_id}.execute-api.{api_region}.amazonaws.com/{stage_name}/launch", + "request_parameters": { + "integration.request.header.X-Amz-Invocation-Type": "method.request.header.X-Amz-Invocation-Type" + }, + "http_method": "ANY", + "method": { + "authorizationType": "NONE", + "requestParameters": { + "method.request.header.X-Amz-Invocation-Type": false + } + }, + "integration": { + "type": "AWS_PROXY", + "integrationHttpMethod": "POST", + "uri": "arn:aws:apigateway:{api_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:{lambda_region}:{account_id}:function:{function_name}/invocations", + "requestParameters": { + "integration.request.header.X-Amz-Invocation-Type": "method.request.header.X-Amz-Invocation-Type" + } + }, + "path_part": "{proxy+}", + "stage_name": "scar", + "service_id": "apigateway.amazonaws.com", + "source_arn_testing": "arn:aws:execute-api:{api_region}:{account_id}:{api_id}/*", + "source_arn_invocation": "arn:aws:execute-api:{api_region}:{account_id}:{api_id}/{stage_name}/ANY" + }, + "cloudwatch": { + "boto_profile": "default", + "region": "us-east-1", + "log_retention_policy_in_days": 30 + }, + "batch": { + "boto_profile": "default", + "region": "us-east-1", + "vcpus": 1, + "memory": 1024, + "enable_gpu": false, + "state": "ENABLED", + "type": "MANAGED", + "environment": { + "Variables": {} + }, + "compute_resources": { + "security_group_ids": [], + "type": "EC2", + "desired_v_cpus": 0, + "min_v_cpus": 0, + "max_v_cpus": 2, + "subnets": [], + "instance_types": [ + "m3.medium" + ], + "launch_template_name": "faas-supervisor", + "instance_role": "arn:aws:iam::{account_id}:instance-profile/ecsInstanceRole" + }, + "service_role": "arn:aws:iam::{account_id}:role/service-role/AWSBatchServiceRole" + } + } + } \ No newline at end of file diff --git a/docs/source/testing.rst b/docs/source/testing.rst index ebbcde17..9127c743 100644 --- a/docs/source/testing.rst +++ b/docs/source/testing.rst @@ -37,18 +37,4 @@ Procedure for testing: Further information is available in the udocker documentation:: - udocker help - -Testing of the Lambda functions with emulambda -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For easier debugging of the Lambda functions, `emulambda `_ can be employed to locally execute them. - -#) Install emulambda - -#) Execute a sample local test:: - - sh test/emulambda/run-local-test.sh - - -This test locally executes the ubuntu:16.04 image in DockerHub via udocker executing a simple shell-script. + udocker help \ No newline at end of file diff --git a/scar/parser/cli/__init__.py b/scar/parser/cli/__init__.py index 777a4346..f6827290 100644 --- a/scar/parser/cli/__init__.py +++ b/scar/parser/cli/__init__.py @@ -89,6 +89,9 @@ def _get_lambda_environment_variables(lambda_args: Dict) -> None: if "image" in lambda_args: lambda_env_vars['container']['image'] = lambda_args.get('image') del(lambda_args['image']) + if "image_file" in lambda_args: + lambda_env_vars['container']['image_file'] = lambda_args.get('image_file') + del(lambda_args['image_file']) if "lambda_environment" in lambda_args: # These variables define the lambda environment variables diff --git a/scar/providers/aws/functioncode.py b/scar/providers/aws/functioncode.py index 0926bd37..50d75f57 100644 --- a/scar/providers/aws/functioncode.py +++ b/scar/providers/aws/functioncode.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from _ast import Or """Module with methods and classes to create the function deployment package.""" from typing import Dict @@ -68,7 +69,8 @@ def _copy_function_configuration(self): FileUtils.write_yaml(cfg_file_path, function_cfg) def _manage_udocker_images(self): - if self.resources_info.get('lambda').get('container').get('image_file', False): + if self.resources_info.get('lambda').get('container').get('image_file', False) or \ + self.resources_info.get('lambda').get('deployment').get('bucket', False): Udocker(self.resources_info, self.tmp_payload_folder.name, self.supervisor_zip_path).prepare_udocker_image() def _add_init_script(self) -> None: diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index 8d200e84..c96c27a4 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -103,7 +103,7 @@ def _get_function_code(self, zip_payload_path: str, supervisor_zip_path: str) -> S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), file_path=zip_payload_path, file_key=file_key) - code = {"S3Bucket": self.function.get('deployment_bucket'), + code = {"S3Bucket": self.function.get('deployment').get('bucket'), "S3Key": file_key} else: code = {"ZipFile": FileUtils.read_file(zip_payload_path, mode="rb")} diff --git a/scar/utils.py b/scar/utils.py index c0836e5a..cb38fbe4 100644 --- a/scar/utils.py +++ b/scar/utils.py @@ -80,7 +80,7 @@ def delete_environment_variable(variable: str) -> None: def execute_command_with_msg(command: List[str], cmd_wd: Optional[str]=None, cli_msg: str='') -> str: """Execute the specified command and return the result.""" - cmd_out = subprocess.check_output(command, cwd=cmd_wd, stderr=subprocess.STDOUT).decode('utf-8') + cmd_out = subprocess.check_output(command, cwd=cmd_wd).decode('utf-8') logger.debug(cmd_out) logger.info(cli_msg) return cmd_out[:-1] From 644de2b80c8b2eb0ba8b786f0273ba87d60099f3 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Fri, 20 Dec 2019 17:42:37 +0100 Subject: [PATCH 38/41] Update scar's documentation --- docs/source/advanced_usage.rst | 2 +- docs/source/api_gateway.rst | 155 +++++++++++++++++++++++---------- docs/source/batch.rst | 83 ++++++++++++------ docs/source/configuration.rst | 5 +- docs/source/images/homer.png | Bin 0 -> 465418 bytes docs/source/images/result.png | Bin 0 -> 304212 bytes docs/source/prog_model.rst | 129 ++++++++++++++------------- 7 files changed, 232 insertions(+), 142 deletions(-) create mode 100644 docs/source/images/homer.png create mode 100644 docs/source/images/result.png diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index ea666083..9c7926db 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -94,7 +94,7 @@ or use CLI parameters:: scar run -n scar-cowsay -s runcow.sh -or a combination of both (to avoid editing the initial *.yaml file):: +or a combination of both (to avoid editing the initial .yaml file):: scar run -f cow.yaml -s runcow.sh diff --git a/docs/source/api_gateway.rst b/docs/source/api_gateway.rst index 6f5d4ea3..cae4cab5 100644 --- a/docs/source/api_gateway.rst +++ b/docs/source/api_gateway.rst @@ -9,11 +9,14 @@ SCAR allows to transparently integrate an HTTP endpoint with a Lambda function v The following configuration file creates a generic api endpoint that redirects the http petitions to your lambda function:: cat >> api-cow.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - api_gateway: - name: cow-api + functions: + aws: + - lambda: + name: scar-api-cow + container: + image: grycap/cowsay + api_gateway: + name: api-cow EOF scar init -f api-cow.yaml @@ -24,79 +27,137 @@ After the function is created you can check the API URL with the command:: That shows the basic function properties:: - NAME MEMORY TIME IMAGE_ID API_URL - ------------------- -------- ------ ---------------- ------------------------------------------------------------------ - scar-cowsay 512 300 grycap/cowsay https://r8c55jbfz9.execute-api.us-east-1.amazonaws.com/scar/launch + NAME MEMORY TIME IMAGE_ID API_URL SUPERVISOR_VERSION + ---------------- -------- ------ ------------------ ------------------------------------------------------------------ -------------------- + scar-api-cow 512 300 grycap/cowsay https://r20bwcmdf9.execute-api.us-east-1.amazonaws.com/scar/launch 1.2.0 +CURL Invocation +--------------- +You can directly invoke the API Gateway endpoint with ``curl`` to obtain the output generated by the application:: + + curl -s https://r20bwcmdf9.execute-api.us-east-1.amazonaws.com/scar/launch | base64 --decode + + ________________________________________ + / Hildebrant's Principle: \ + | | + | If you don't know where you are going, | + \ any road will get you there. / + ---------------------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + +This way, you can easily provide an HTTP-based endpoint to trigger the execution of an application. + GET Request ----------- -SCAR also allows you to make an HTTP request, for that you can use the command `invoke` like this:: +SCAR also allows you to make an HTTP request, for that you can use the command ``invoke`` like this:: scar invoke -f api-cow.yaml + Request Id: e8cba9ee-5a60-4ff2-9e52-475e5fceb165 + Log Group Name: /aws/lambda/scar-api-cow + Log Stream Name: 2019/12/20/[$LATEST]8aa8bdecba0647edae61e2e45e99ff90 + _______________________________________ + / What if everything is an illusion and \ + | nothing exists? In that case, I | + | definitely overpaid for my carpet. | + | | + \ -- Woody Allen, "Without Feathers" / + --------------------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + This command automatically creates a `GET` request and passes the petition to the API endpoint defined previously. Bear in mind that the timeout for the API Gateway requests is 29s. Therefore, if the function takes more time to respond, the API will return an error message. -To launch asynchronous functions you only need to add the `-a` parameter to the call:: +To launch asynchronous functions you only need to add the ``-a`` parameter to the call:: scar invoke -f api-cow.yaml -a + Function 'scar-api-cow' launched successfully. + When you invoke an asynchronous function through the API Gateway there is no way to know if the function finishes successfully until you check the function invocation logs. POST Request ------------ -You can also pass files through the HTTP endpoint using the following command:: +You can also pass files through the HTTP endpoint. +For the next example we will pass an image to an image transformation system. +The following files were user to define the service:: - cat >> api-cow.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - data_binary: /tmp/img.jpg - api_gateway: - name: cow-api + cat >> grayify-image.sh << EOF + #! /bin/sh + FILE_NAME=`basename $INPUT_FILE_PATH` + OUTPUT_FILE=$TMP_OUTPUT_DIR/$FILE_NAME + convert $INPUT_FILE_PATH -type Grayscale $OUTPUT_FILE EOF - scar invoke -f api-cow.yaml - -or:: - - scar invoke -n scar-cowsay -db /tmp/img.jpg + cat >> image-parser.yaml << EOF + functions: + aws: + - lambda: + name: scar-imagemagick + init_script: grayify-image.sh + container: + image: grycap/imagemagick + output: + - storage_provider: s3 + path: scar-imagemagick/output + api_gateway: + name: image-api + EOF -The file specified after the parameter ``-db`` is codified and passed as the POST body. -Take into account that the file limitations for request response and asynchronous requests are 6MB and 128KB respectively, as specified in the `AWS Lambda documentation `_. + scar init -f image-parser.yaml -You can also submit a JSON as the body of the request to the HTTP endpoint with no other configuration, as long as `Content-Type` is `application/json`. If SCAR detects a JSON body, it will write this body to the file `/tmp/{REQUEST_ID}/api_event.json`. Otherwise, the body will be considered to be a file. +We are going to convert this `image `_. -This can invoked via the cli:: +.. image:: images/homer.png + :align: center - scar invoke -n scar-cowsay -jd '{"key1": "value1", "key2": "value3"}' - -Lastly, you can directly invoke the API Gateway endpoint with ``curl`` to obtain the output generated by the application:: +To launch the service through the api endpoint you can use the following command:: - curl -s https://r8c55jbfz9.execute-api.us-east-1.amazonaws.com/scar/launch | jq -r ".udocker_output" + scar invoke -f image-parser.yaml -db homer.png -This way, you can easily provide an HTTP-based endpoint to trigger the execution of an application. +The file specified after the parameter ``-db`` is codified and passed as the POST body. +The output generated will be stored in the output bucket specified in the configuration file. +Take into account that the file limitations for request response and asynchronous requests are 6MB and 128KB respectively, as specified in the `AWS Lambda documentation `_. -Passing parameters in the requests ----------------------------------- +The last option available is to store the output wihtout bucket intervention. +What we are going to do is pass the generated files to the output of the function and then store them in our machine. +For that we need to slightly modify the script and the configuration file:: -You can add parameters to the invocations adding the `parameters` section to the configuration described as follows:: + cat >> grayify-image.sh << EOF + #! /bin/sh + FILE_NAME=`basename $INPUT_FILE_PATH` + OUTPUT_FILE=$TMP_OUTPUT_DIR/$FILE_NAME + convert $INPUT_FILE_PATH -type Grayscale $OUTPUT_FILE + cat $OUTPUT_FILE + EOF - cat >> api-cow.yaml << EOF - functions: - scar-cowsay: - image: grycap/cowsay - api_gateway: - name: cow-api - parameters: - test1: 45 - test2: 69 + cat >> image-parser.yaml << EOF + functions: + aws: + - lambda: + name: scar-imagemagick + init_script: grayify-image.sh + container: + image: grycap/imagemagick + api_gateway: + name: image-api EOF - scar invoke -f api-cow.yaml + scar init -f image-parser.yaml + +This can be achieved with the command:: -or:: + scar invoke -f image-parser.yaml -db homer.png -o grey_homer.png - scar invoke -n scar-cowsay -p '{"key1": "value1", "key2": "value3"}' +.. image:: images/result.png + :align: center \ No newline at end of file diff --git a/docs/source/batch.rst b/docs/source/batch.rst index c8038356..8116264c 100644 --- a/docs/source/batch.rst +++ b/docs/source/batch.rst @@ -3,7 +3,7 @@ AWS Batch Integration ======================= -AWS Batch allows to efficiently execute lots of batch computing jobs on AWS by dynamically provisioning the required underlying EC2 instances on which Docker-based jobs are executed. +AWS Batch allows to efficiently execute batch computing jobs on AWS by dynamically provisioning the required underlying EC2 instances on which Docker-based jobs are executed. SCAR allows to transparently integrate the execution of the jobs through `AWS Batch `_. Three execution modes are now available in SCAR: @@ -19,30 +19,49 @@ Set up your configuration file To be able to use `AWS Batch `_, first you need to set up your configuration file, located in `~/.scar/scar.cfg` -The new variables added to the SCAR config file are:: +The variables responsible for batch configuration are:: "batch": { + "boto_profile": "default", + "region": "us-east-1", + "vcpus": 1, + "memory": 1024, + "enable_gpu": false, "state": "ENABLED", "type": "MANAGED", - "security_group_ids": [""], - "comp_type": "EC2", - "desired_v_cpus": 0, - "min_v_cpus": 0, - "max_v_cpus": 2, - "subnets": [""], - "instance_types": ["m3.medium"] + "environment": { + "Variables": {} + }, + "compute_resources": { + "security_group_ids": [], + "type": "EC2", + "desired_v_cpus": 0, + "min_v_cpus": 0, + "max_v_cpus": 2, + "subnets": [], + "instance_types": [ + "m3.medium" + ], + "launch_template_name": "faas-supervisor", + "instance_role": "arn:aws:iam::{account_id}:instance-profile/ecsInstanceRole" + }, + "service_role": "arn:aws:iam::{account_id}:role/service-role/AWSBatchServiceRole" } -Since AWS Batch deploys Amazon EC2 instances, you have to fill the following variables: +Since AWS Batch deploys Amazon EC2 instances, the REQUIRED variables are: * `security_group_ids`: The EC2 security group that is associated with the instances launched in the compute environment. This allows to define the inbound and outbound network rules in order to allow or disallow TCP/UDP traffic generated from (or received by) the EC2 instance. You can choose the default VPC security group. * `subnets`: The VPC subnet(s) identifier(s) on which the EC2 instances will be deployed. This allows to use multiple Availability Zones for enhanced fault-tolerance. -More info about the variables and the different values that can be assigned can be found in the `AWS API Documentation `_. +The remaining variables have default values that should be enough to manage standard batch jobs. +The default `fdl file `_ explains briefly the remaining Batch variables and how are they used. + +Additional info about the variables and the different values that can be assigned can be found in the `AWS API Documentation `_. Set up your Batch IAM role -------------------------- -The default IAM role used in the creation of the EC2 for the Batch Compute Environment is **arn:aws:iam::$ACCOUNT_ID:instance-profile/**ecsInstanceRole****. Thus, if you want to provide S3 access to your Batch jobs you have to specify the corresponding policies in the aforementioned role. +The default IAM role used in the creation of the EC2 for the Batch Compute Environment is **arn:aws:iam::$ACCOUNT_ID:instance-profile/**ecsInstanceRole****. Thus, if you want to provide S3 access to your Batch jobs you have to specify the corresponding policies in the aforementioned role. +If you have a role aleredy configured, you can set it in the configuration file by changin the variable `batch.compute_resources.instance_role`. Define a job to be executed in batch @@ -50,27 +69,39 @@ Define a job to be executed in batch To enable this functionality you only need to set the execution mode of the Lambda function to one of the two available used to create batch jobs ('lambda-batch' or 'batch') and SCAR will take care of the integration process (before using this feature make sure you have the correct rights set in your AWS account). -As an example, the following configuration file defines a Lambda function that creates an AWS Batch job to execute the `MrBayes example `_ (the required script can be found in `mrbayes-sample-run.sh `_):: +As an example, the following configuration file defines a Lambda function that creates an AWS Batch job to execute the `plants classification example `_ (all the required scripts and example files used in this example can be found there):: - cat >> scar-mrbayes-batch.yaml << EOF + cat >> scar-plants.yaml << EOF functions: - scar-mrbayes-batch: - image: grycap/mrbayes - init_script: mrbayes-sample-run.sh - execution_mode: batch - s3: - input_bucket: scar-mrbayes - environment: - ITERATIONS: "10000" + aws: + - lambda: + name: scar-plants + init_script: bootstrap-plants.sh + memory: 1024 + execution_mode: batch + container: + image: deephdc/deep-oc-plant-classification-theano + input: + - storage_provider: s3 + path: scar-plants/input + output: + - storage_provider: s3 + path: scar-plants/output EOF You can then create the function:: - scar init -f scar-mrbayes-batch.yaml + scar init -f scar-plants.yaml + +Additionally for this example to run you have to upload the execution script to S3:: + + scar put -b scar-plants -p plant-classification-run.sh + +Once uploaded you have to manually set their access to public so it can be accessed from batch. This has to be done to deal with the batch limits as it is explained in the next section. And trigger the execution of the function by uploading a file to be processed to the corresponding folder:: - scar put -b scar-ffmpeg -bf scar-mrbayes-batch/input -p cynmix.nex + scar put -b scar-plants/input -p daisy.jpg SCAR automatically creates the compute environment in AWS Batch and submits a job to be executed. Input and output data files are transparently managed as well according to the programming model. @@ -83,7 +114,7 @@ Combine AWS Lambda and AWS Batch executions As explained in the section :doc:`/prog_model`, if you define an output bucket as the input bucket of another function, a workflow can be created. By doing this, AWS Batch and AWS Lambda executions can be combined through S3 events. -An example of this execution can be found in the `video process example `_ and in the `plant classification example `_. +An example of this execution can be found in the `video process example `_. Limits ------ @@ -93,6 +124,6 @@ For example, the Batch Job definition size is limited to 24KB and the invocation To create the AWS Batch job, the Lambda function defines a Job with the payload content included, and sometimes (i.e. when the script passed as payload is greater than 24KB) the Batch Job definition can fail. -The payload limit can be avoided by redefining the script used and passing the large payload files using other service (e.g S3 or some bash command like 'wget' or 'curl' to download the information in execution time). +The payload limit can be avoided by redefining the script used and passing the large payload files using other service (e.g S3 or some bash command like 'wget' or 'curl' to download the information in execution time). As we didi with the plant classification example, where a `bootstrap script `_ was used to download the `executed script `_. Also, AWS Batch does not allow to override the container entrypoint so containers with an entrypoint defined can not execute an user script. diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 8a29a1d3..d3ff0f73 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -46,9 +46,10 @@ Configuration file The first time you execute SCAR a default configuration file is created in the user location: ``$HOME/.scar/scar.cfg``. As explained above, it is mandatory to set a value for the ``aws.iam.role`` property to use the Lambda service. -If you also want to use the Batch service you have to update the values of the ``aws.batch.compute_resources.security_group_ids``, and ``aws.batch.compute_resources.subnets``. -An explanation of all the configurable properties can be found in the `example configuration file `_. +If you also want to use the Batch service you have to update the values of the ``aws.batch.compute_resources.security_group_ids``, and ``aws.batch.compute_resources.subnets``. There is more information about the Batch usage `here `_. +Additionally, an explanation of all the configurable properties can be found in the `example configuration file `_. Below is the complete default configuration file :: + { "scar": { "config_version": "1.0.9" diff --git a/docs/source/images/homer.png b/docs/source/images/homer.png new file mode 100644 index 0000000000000000000000000000000000000000..fae13c614132449735b72c354935e86db0bef645 GIT binary patch literal 465418 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%0D7NOA?3L>HO7eb9<6~o3 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 literal 0 HcmV?d00001 diff --git a/docs/source/images/result.png b/docs/source/images/result.png new file mode 100644 index 0000000000000000000000000000000000000000..25c3414c4ec7608e5546ab355e14a09bd7cc9b70 GIT binary patch literal 304212 zcmeFZhdQ&oOp5Kw!ECC+t5;-YckD7-O~)IsC+x`EpEKZ(pCbmR7b#UP*~?zl+`QQ|6W}&+#ppPW+5xe*SsP^bvVJ z^2L2egz*#K`{Lr_tgNiEva-s`%Hm=n+EAX5wcjr({_O1@e~WsS^AE77`QHZ5=CgnO z`ZY5%6B`>l?<^UQKP`t2mKRPSHJZBat8~%#VB+9B#`jFIp*ppK`d@tobrF@Tq)kF$ z>Y0izi+#&^?^4^h{Hc(T_8_7D?1%2|dUMUm!o_8y?}NU$?SPt*(c|IrS@+c0bC@(k zjH%~%{LyK@Bf%Haag4f;TbN$5x6U{^hUVuNF6zadc5?gu+2>{eDGv{igW=Zf^&|Is zOJ5`_rkT`ET)-*?PENn?}ht|v%D?0+b8_eX2AVEe>8$u9AY`!9!N zw2qsG=2)YWwW%+~8oZC1$fcI5W>fK7X2)sfL4+KDHSTfEvKVp$4$PNR?!D8< zvlUyEAb<75sfsyyps^7Ad8s&?;Bj_F2FNp@!vS9jKc7@#_AYVJ?bxHk$S4BqazhDB|)AMo&H?m zW}_XNIKX7P{z+mEVXw%7xy??i>S}3mYqe^=ZqT4^7U{4O2^qCFqi-!-#NOo%In_@`MfZfl_p^&3#*Z(> zpRRDx)Z4KC`M!k({Gz3bX!xN6bc-~VL1 zzOJOCq@cjIztC{dR$IiZlLISZ;Cja|BQ*W;M+z@<`ZJr6&8ETfjU@%FGS8fshqTmR z-=vV~Enkk5-L+~Cp+P4^Ox>u-kk*2`Fw~K(j;aYTx$#;tvr8V1|KgpycWa#IHPqD5 zku68F-iMZ|{m-Yu#dqtC)z#IFjg4_QoW6cio7wM&HHGi384GOZshTd3`+gQtN$x6N z?(Xi6=DZcbD7sM5a9~$|{RR@R&Kd(}q2z8djc+}A^oYq$dTYd~|AT&o=ibxda*V;m z>KQrqIvm>iMx1e+$+v-1{!0?%UZ?Zs-N*iPB>AJO3j_T{HoxZDdq~t_QCeCiJW>%4 zaoXeco_m|i`WhM-u>ERaH4aCWqIk#B%S}3*NVJKnLl&)tWFdIJ9!NG84=3(K10Q{7k?3 zd~?Yo3db@~2CkejCecMoTG zx8DULe{^`DQ)rMtP<@5ub_xnf?Li8Q?{7XnY>6<($9=$+p2?uzsdFP^-)wBoU^g`h zXACf*53o+GE_{?2zl^x9#EdLXYS}8A6}T0=qdLhD)Z2ZE9N5~Z-Vl9O!vIAo!#;+HqWw$E#wk)_MZIsb~9lOq9*PD z6a}3bxi5RP60^fs%3CZhy(3jl>%xXtEq{!_NmRQm#t9myDsxR(Jp4ATcS8I88B%)t zrH?I#^QzwMTbH5u_(hSEBX{dTQZeF)hcmyvw^aJ+QGdInYGO`$84X9A`j###eeoHZ zSV2J{J3CwE*}u7-cN7a^vyN4s&Js`@v-9&8s~7t|JYVk+znVbW`>A&zKVLF}XoEX5 z$DFTh=?jV7V2vvwGg7wNl`uvj^G}WSvi-)wueY9?gYXUiqj79Ip*Om9;Opx<6?y#- z(x-NBrKVxyMX6;cEq26p+;gXzGL>z+N$3joD+FTiV7zIn3Q7bE(WhMA zXlX4EeR_ms1U@%zs5bb#{5+=_m$2xTfppAP4!B#k{=UW`Z3A3_n z5{mjU^iK6Hgw^KC=w8F&Vgvb~EI9ViS$9?0oh-ZIkc^8lZAuqU@$mWuHJi>m>-C6i ze}^-NFKE^@G&JbxIU+vBA(Fd2y6UefITe$iE#&Qu?dHzB5jJmcVe?*cH{y!CRz@oDdpb$PwP@zV8ogG`XA?Ge|{aYGz`r>=K z%78rfGAKY0ZiJ+OG6%vagf_LiA-aujOLLnFr7mbhdKYUQgD9X<-w`-YN zb(45lB5gM!YYX%?RL`_z7mjh3?`n@TDOlq;9RG@k;0|H`Ylije| zIYHWe?KGW%(6fJ5N>5@{2lpyI5GWvfg>!xYQ!tZdk+pj2sky%0L4R4aZ*f zTV%`G%(f_^n2W;Dny$)t>;=hOA44k_ZAn76$ zGdrG-0Z^DL?UcPw-Rv@ICY-k@i7h(DHwSxLZ^i6}wpXXR*xcCMCxb8N+)H`3zuMAb z`ky;Z>sf#SLSd tSYoZd-w#30#32S54!>xKY;@RQOY|=q35|dJd+V%N&Zy{_% z9^%aIYW;GP_L`2q6~SLXN(iW#cod9Vp>zark}0vUE_HYkj4Luuz@)_V$xTo_EPGk&Hbj$nc$|rlsYI zX;bDZ1%RLsN;~u1`8cdlNLV@Bc>T*{IF!e1Qi^56cdPK6)S-tmHA)Frt>wUex zKS81ML2m;dUw`w$4>RFtC1U>^etaTCc8}`y(b~()FeZ3<$|NrXBz^sx9R)Wh>9PIS zQ&s5kwUx@Lh>gX5LEYjLp{Fu1WK!{XL$?Y44p}5-Q1iAz0N=Xv+&6ydh(8$%Te#2N zNKNchxEL%y38h75sklkcDz><`cK;=XY}BPZ^vZkdL^KCga54Z_qO;_-n=wYvj7O_o zE?{%>Lu8wN?iWEJfX}1ih$n3I;7cjghz4{gIH{;Te=S^hdSsHoXhwBg^=B0x$ zRK+R8nY*Xbg*N|?*&1m$Iw0^n|8gC1Xo2EY`*>7RgpCSIA3#Ygr>@=wNwzbW8CgE! zN1&U1>wTE?q&`^q!Cc*v#62jp0KK9J>6B1{vRG~+$8j51>$bY^>%BjkTZ+%G)uQzD z>C=#)GEfrk-npZ<(hHFqu~=PhcUJDjWM`L^NK!{)j{8F1J6dY$vXQ)(>=MxM11Z>8 ztv@>?8wkYT$u{!b_!TEbI21Ph8U#(7Q6b#~&lK>E~9+U0Amqhl3=7xNHYY zybl(QgKj8}XR@P|dzMCPwz6)=XllVP9t}~wgKqHN7!JO=TGFuJbNtp9{w5)VetvFl ziG|!um|r%a5PhKGGMR3TUhFUGoN=rtELKNI>R(QA?(PEAL#W#-Z#=Q-f8m~l$?9Y? zHi#WHxhm0E-YGE^<}meb6s~@Ul?4frAvUC}q(nd$@&dSqDp2s_A0o%aiJE>z8TNBV zE0Lr;`<1U{pTyE4N9>3uE?fklV*mRyObdPL4cH(k&}@#an98SFG9;KdK3oYrS|7vb zdLffWzmr5~h~*yOv>|piKG6vp)jG}o@Sq4sI{z{iz)z*w$>-yHW6*??@4fRJudris zd8gc?No@{~1y|y|gEwYStsN&f@1DT&O_?EFyHFm zuoWYr=-WEDNq)_&JL@)ouf=Bh@Z$^E+JnvEyYT`F--@7C{*IWUrlw|w>?o>TyOZf^ z_;k>`8!hX$QoShP+~dOhuD-q=LMcHR$dPLi>u{P<5ig(C1k0Zqb!~}cV~cF@N7oZz zDnT$S49zH?Zrt3MpEoAG>)Y)f5yO5_m8O$43SVTm8x+-F6WtX`h3f^3VLl9PK6mJY z_0UYb@!|EO_3Qkr(Z`9FnKASH1Yjp$1CQ0_PT4&^BV)(5OYe1x6#ww&tC~EQW@Y=& zRGuWy7~@CSGdDMfDAv)UIg_(got3`;V@@DB%g{eVjRPZ>Us~88qQ_w(p1c1R78a_j zt07Z=_4b;T;$C|~(^{`(M`b6z!rsq?put0!MD*u4IUygEZuyf7VY2SqcPY@> z4VN#&h5I~cQtT1&M+2xk0Cq@ku2y{9qtt|6u=Xz&devez7DYYcYp~S!VfZ!A{Y3n? z{m!F()$M8kp8!FjX@2_jsoZX=;%BwSW>Z&(nXp>9+6NT*mNZ517c8ntDD4R)SGf43 zT87*id^pF&Fn@IF?3csS_OC=$LKQRpkG}xv@W4RK&CSij^Pfsj`Er%htf|}egLzed z^n^wy#N3MSX_CK#JRznyZ8S0R^S={K2^7cUil^W4BcX}EK+#L@iSi)H?gvl}=~*`7 z4ii-U{u&;Ll*+=;!gBgB7_!QsYEjeBuzX(?1gteQGz2HFc|{9{OX#pL>~}Gw!>Z@s zS5lLg!!PHaJcA<<*Hxz;2cj+T&aF(Yow&cgJVsN-T3W06JTF&^Oz0&zJ)U6X#geqN zK9!f3XJnlE=6I0`kaKP>H)A+09i1u+@nf~_*dU0}CZR?h$)`&LFx6D%;yujpF)z`CjJkRcn)yiRRiT;!$-ws;UAo@|Fg8p&xzi0% zX+gSA4e7LMy!Mvo+B6Lf z4b{~@0(%Hw`T19d%bW7bbB!DWQf?FXKrIA9l^tn2Y|Hh3vrs3pxKgt7m>~ z%`q#fx-irVSl<8&Hi4He`lotC02ChdE=lSez`9nZ+z}d&NO#{c1W(_9>ERGdAMMk4Fu7Z!*StDU0E1Qi~{O^Z3 zE)I2C?CyVj>mk_(Oe-v>rym1bqK8wb{+hrUUuoHy`cbzaH@CHfkG?h}1ArIMO8F%v zYjs;=c#BeKwqqscBtKZg75~$z6bLeM>nbsgV?JRfHsaDF^8Wq%hV98vkMYOH*mUt2 zo9gDMzO6TH4!LwU?(5X*?(Q>;pE<5@k(^C-zpUrpUbCQAm*czo`J5ymcjMd4*vVCT z19uAk_f_VEQUGy3|@odVJgD?QDqM0AS&0X(gU* zt=QqTfuLy#W4N!UxBHohVtSADBGT}OeUa|SSqd`+@yvP5gKv$8cD)bF2XS@JL?!3i zC)>8^65i*s`}1qCg#=vG&(qB8;yy3`g`-cz?(>V2FVB!M3O%^dnJNRRef}Nw?ghc0 z&2$SDfX!Cj49exG$b7ZcshfoO@Ytu1b(sPM)4SHV&8`{+Zrviu25fiRSXVb_x10XV zEm`hg%Da zD+=Q=e$;aP5cGkQK9^s{@#B4?uBu#^FB&*WP`GVIEgmPDEUF?k9d+|t1d9|19o^j; zU$17qj{p8bROPMc1W~^7prCP;BSM$U9__C~ zmqCgH=ht_^Rj;b`P(H?O)w1D-fRU)vH(6oc>Jj#U0;cDnirE7Z>PPkUNx? z^he}a!`;|aUBwC%>V3*DC?I_oa~skmjqMfs)B;_%4v0NYc}$`zP$>1`%pfpj%vz(y z>YmR!UenE!2r|dzo~8*`1o6$H)QxWPJ!o7%1?ghTNSmU_!i!&8weBg%e?+-AOjJJV zQX|}nYxNyv+~LBaNM)so(Cb(fzQo>zUPvPv)7mrdEX_*M-x8n~m)!u<+pikeC;cwS zAdUP3leZfXqfh|Ctt{!6)EYF1Zcg7#;q$X+wuBm|UvA4F?Pgz3xP{Cp(`?Ax)H2=9 zTQmZrL!}#4J0BT?aF+iMiAQ~Wo$F9m5lZ59p>S0$#%q!I28vRrnzmp~O z*pIq%GU;S@IMvIhmU!~grHiChynaF|S8b=yW6<#e2l|wEm{=v9=Za051nT~ELqdX{ z;JMmN$0YvPpux-YO6;pw`lyo+nRUYNIvMttFFQ*@cQ?WHfy8nvjJ^w$M}(@Ud5k@t z!$gnguo$@i(5UK{t;|;~l=)~MrC%*Hs9IYrYA~vJoDx>4_)nBNg|ve#9v7g;B<=P$ z3WLPx^SSguj$Jx!euJLBJZ5JfSm&O{NOzX}Lwln|@JZB*+-Y62*43-J&Y9q3PoCX^EBi-M^tn zMF&MkyG4H_@nqs7zxQNq1OY}pp8b)cW-t*TV=v*42C>oa76+waKOjFyt6SlWqQI&` z?QAou7v>l$w{y!<=_EPH$Jia?U|4Zyy%KI>>SB1y_rJ-H z5azoZi{ij9L8Tb=K04@dxr;Qvtng#*=%lQ9Nh<`o-gC z8bLQ03~duFpp-Z`{HJ%`|B=B--09s1ev3X`|M;F?C#3qhW_mWlGzXL+&@c$$B;~qd zQ2gKvu(b^)O=qUhfils5>FZm3`!DFd_;|r_5Wuaic@Uij-R+)+l!h0~Kqdfhn^)#d zc~;7OZ93lAYv%#^9`LW+NgdxzKPDz7BFte=j>^3AGu3M!f9e5Z(l$fxA%fW*5L_hv ze;l--Cbm&EA9YDZlPN{RPX=bjCmp5-?8Br62LY->oO#>wn&Ho;l2h7lwkpPfgYjy! zG6!P&V>zdcELgl3yW}stuj-O!Z^VgAADoeUvDQ^(@ED|BKvUCuga%_BowFwm)YR0p zwEQcb{o?U)>TWWSu=np>^AG-l^sB%?q+bzfXln=1X$a3(k3sgll;$%HyAJ#n@0f87gd>!!ULqmG_l7+=Z z7sGg-9Fs3RS2azPY!b!bfOPqC^7HM6<d zZxMZ?mWY{le{b(^zNnUx0UEw%JtDw@4CFa_=&M~3(NB-D8 zmX&ZDL}t7Q>Qs{u2&(lBEe?jR&|5o!Sq89y_9t0Bzd@n`{)>CP0999~S}s$a!!6@b zl*B%SJz$=q6=zbdh6p++pVG8>%6v1EyO1Ixj5K8D6GX7>5Bgn7kpaquxn*72us@KL zVQqN)aC6jkybhQZz{peCRwDV6Tn+@_!uarKN+jt0dZ#>2ybA);|aXTXxsYbc%WDF*s2*<$<`>* z78)YonCs$X<~PvMu}rk+*)n>*{0RW|5vYbxeneI9X2K-_afOR>Ht!d!(HStDLJDsJ zIRXU~O&l?mn_mq?doi;@{~$PcsI~1m`&}zOc+NaO`AR370=yHGtCi+B$4+~V|9%x+ zOgghcjbDsrm&Ty(FGEy-!SE*pJ+8hUzWyyLJ`V`|Qc&y^kErm3(?vGZ& ze2k0}aWJsHetjc$;zfyY+>*^*`PSOg878KWY;4qMnaFZ~D$~mzL6_N@3>7qjZ;3?_ zC%?~6RJu4VTTc+gm6$=~?$k>lvCd3=%;1GAEZ9)ylgHv`Ab}y`nnJczy?0|gWbB5( z>d}tJBV7&bTJnsUT>?~c*&*e8k0!qcTc)~O0AN>BKliODgS&9?jGWs2`$VFre-R&y zlv=9h={S6oQ7{%)AywQjMT4E{)hnvr`d4E`wz1R#45N7i_qCS)mbG3AQ@RL}pFzJ0 zR}4@ttdaQ0+ad!Sk#m-d#aAUerTnuM?u6CGGmvO{X^ti;ON#pH;_=j zhMuDb-xoDhqmpob-#C!j^Yh!x94q#CL)MVtYj6ChFfWf1o7VBwHZgWICJlNt=F(e5kCXkIiU^&9QS)x8X(kLPdlJFk=jj5(*&w zCKT0p#}B9&h|>#8By~hEiLWGBrdIt)d}MZQ4%B-0RJ&m; zYPcNyTgL7)u_OYO?BhDO5u8fogCL2UU=5?Nz7l#*A+yWA)SxX~e5?{+gkb$P(2J)O zl1w(+nbm_PQe~c}+BcrFOm+w;J8w4}(J}>ts85MR^ppf(2`w#KYAn!UKv;dYrhcAV zd#OX8SS%2a!z^j5zXqKUlzm^{Q_Rd#p-6NU)$IClcjY$dr%9Ks1}aYoJeV3VughGAwgzN=c8_%#U5Oo&R&-WF{^BkeB)nio zp`)b*mPucqDYE6^M|#_vubW?$^lf<(V*|qtdPH0d$Demucd6y(ML!^lGrt6VVf&8A z`}20sa?5#~x4s@m&^plzpvi`r@n-`}KC2fM6^W~?gX4ff)TZ})!d1{Mz42)$s9PG- z$AH<)5vMr@J`%@;o*A*W{wt2Y1R4@J2e3iz>wlnftDqb@MEemJ6hiCPd$oalyaTa(oB*RMM>HA>T)hGZ<-agQFmmx@!#f+G9^lq->aW36L9RHl z_G99Xj&M5N;l=NYl;``xTc$u$0GTJQzcI8pisW()E9qHpKkYN?>>oy3WjXz#w7&jK z&j%*?<4;uJx5waaaYGDH<41emN_F2PPp@34b%p)|$G44G^#}>BekM)krZ#_z8-O+Wz z1&nbeg^S>?wcP~A+3?d!DzG`q9Zfu;iTj((8I3B5NMED|$IVs7aJ@MH>aP<+_xQgU zMU2GBOn9>COI*;^<^o^~f4(4al_yJsr*qx(CX^XyYD(!YvoFZQ1K%lK(0mSFdK zi)3#|qyth)p2LSx&t!vl{F18k1J4&*vZ;z?a%9Z)3-*h7f1fn9`kG7_b!;z@Hs!v5 zUurjuxDS|HK>TTZ!qQUpulp_>D0qg2y3}mHBkomoQV+-tAlGUD5UtCwAr)!o>2dQt ztLx-OCN4-Dit!R<3@bOSi7?b^&f$t}a}x&auv5 zr_CziJD+kV8~vWIxg33poPS@xE6svZWwfk8oij_w$&l`JHjJkrwcNgaTZEB}w>#(6 z#_z%9lf#A*|LVi8B_A+tE~xZ48&>4xa3ytE2s8MVSPC;_MP#1p13SrV=Uc=W2uIu_ z(xo`yGOBbjV{vzLdDs|y3pth^@O$eryvL7m=6F1KX}cL3)CXz<(0jq)LPnfB^}t<% z3wf{J99LpN^Q_52H~|q&!9QvOH)8gIbIu4ud@5ai?+PL+s5|?|S`-oXBBnkwz2^F0 zrF8k5+f!asrdZDai-=}Z7~C#hy0q%Xyjc|1ZMQC-ur<=}-soXEGy~4wT*aC8t6MPp&td#wKy7s$>KOT_)B%{cg#gB+@ zW&a~He4vLLpe7{9FLF@dlT1NSjX?Y!JP2g)tOT&jJ!Q&VFBNYUuYoB{p!RR$qr$3h zA|3J^6V;EwRn#(-pP$oXZP>3)O#dm`4=tep;4@uTu_PpR+afM++ahb_akR@j@S~At z0@G1kjIK7kG8Pw=r*gKBirfkR@f~_aW?ArhPR=l;M{0+~M@@Adoxn<0w?lwmG9MvN z2wqz?hyka%u)nX*^vkK$zabf#nwq2x!WYL&k$xRjCx>VrDCv#f`v|vBO{vGg1HQ7| zLc~j|w-x@nG|sW`fjr802~bOu$m}ZOVaRhwss|Hu=84k!Rx$YA%y= z#aHrtSnnd-J|mvOT<|O?DwCBwi;eLTKtprL>3Gfe}{=AGCeTillI?=JVtY=`yNMvqlhD#`^uG&X4A7m z^p2l@_pE}vQ%m|t^<9tdyj8FLz}CSL=)$!#ar#l|;diElH4s|`43dnS`{3X0n=^=* zdLW8KJ;vRqQx|yKMsRitB+=Yf6--bD+rMXMzD}Gu=E8OmXGA3op9oF&nE}(WNAmj0 z#V8Yc;aKqzhg%+{`x@KJ!oG}N!oF{#-A0zG>iJuYhL+1o8cAr^{0Vyr%HPWE2AS_F zX6eVxRIhA-u?F-l{Y?$9GzRI3t2hBTkKt0W$@lU{BL@US%xEtI7kn_K#kBuv`lQN9 zVq|stT?Z&!iZD9^E-_;2BGtLqp7qnO?jl3r?(Qyhds{K4(B@Zd7qQV>EOQ+DBzxlf z`g0t$d<;C?rd_H)2UO&A378Hus-%O7yzqpKoqm^P} zb*|B$eIICV=Q5D&Uy0|Gjqz>ioGc=Pu}qnC)kXlub#*nVSnI0NQ}Rzy#b9 z*dQpj%c-+0FNpZU<=lSQzsxrB&GEHqRNN7$GVH6w9`NHfab>fU-Fv?4zo5JnKj)(x9heRK8}>#CfXfPjagE2Pe~z^qR(Tn-bvo~%!pjvJ zvPK%F|ETtB;vavOL+i$Asi}cwrYG5?ySugfr*34F>!~=QG#|r$h1*woUa9Z3uh+gW ztX_c6@O3Q&@n%oew_B8g;b#me62zY%CNmFXZ46-%`B;*i$3O!O9x8)>^9r&b2z%vr zne&uXFEYF4z^wodEO{-ht|dBuZ+~=teuj9rSIU0{F6NiA>`&Qe?%r0pYhq$qCJWLi zwP8Sl|)+&fMU-DEun99JG4}+9U=gq4aEBd+HU$NdWd&$7*bxoqiQZyxm z0=Krn5jMH4gdu-lAX7))V2GlOY`G9j%-DG+DVodvz0V-I>ytQ>W#J7vh31bLKAveI zu#u98=}5k|4!$DRS3WSW_6ZyGe*hI5NT+&1fJ!LL(RF0-SPp{<3*AXCZlk~9Pz&YI zOrv9!SPtSBR0_;L5vt%c8A16V>%_zyu%m7aJdBL3))ARgO~@np!ufQzXQl1chNm%j zA^Pvan6Q<0s>;BL_}~v3hCNut1)s^|a{&+pK4^Ufh32C}!chjd2=l|?a^T}7!AS+=pghU} z=!(k1n&GctyM)Ga>i98871TkH=+NwPcLA9>V#*KCC|CHPlK^5Ycp4d{$DS-VGOqZS zZpo;d;Mb+`CCWYWS?>#I_!#x@lUWmoH-!5&`&|Yaay|XSmkYzyZYe0XhMsX>sup;5 zCp5sci$n7lA8lv{Xtl835QKVhH%tZyn5V#AkYJ)fR6ZV`B?bTYS^8a^szx9udHlA= zw(2Wj00#C4`UwZ-{z`yGT|GfHL3jp+r!{GcOKIs!3fv?*BCSaWDm7YKH0`u^@_<8} zXyTymb6^JJcO2iwtKQi-^Vrv~b>sZg8`6U(e z7tHlJQf6}i(WSvsFv1#VK3Hy-_(vaX25d*GY>0Vq-4lsGrFX}rBLyQGjLxX8eg?d2 zo@RdZ)7TYoj(oX&Ipw0K-bG8VMV>|($4#t2e2)u@PSga1o0RPt%1eeS0+vg;0%}W6k zQ@R}i%Qmx@h&0q6$QD6X%4+X@RcM}sS9b*xKt%^PoB?!3Grc#tB~=2>!WvFM$cQk4 z+W|N}gYn|xYsFDh_Bk-}0c^p7M=#r(B;hrFMnom%*|~Sxu(_myFL`h|C<`cD;JDU?zx1jDp{Qx+Bz21_jgib;IVz|hx&6#^#Ht#NPf z|E-kG*Zw^jitbLW1hln^Reud*A=v$iPG>jnwT9GJ83;hg_sqLVr?#1?G4L{TjRuWg z$*611vL7kQ;Qpw~XJB?0r4ITghd$LcF|#8qi=3Q4QC0%ch$5sKzh6}BTNA>=)k{>t zfYwKd5|sY{EN|ExdPMnCfbNcC)d&a?>SUq+>xoErPIZI0^GC-UK801LK@gc5yXhQq?x;}~Lta{cDx1ef^0)uo>+oG82Id5_~+35Xu~!5H z0v=UdW^DUW_g+nj5%AoZd9X4th}qI(6G2g>p80u`0{AUSoMfj4Dj_GhvA60>V|V+?h_s!3Cd@zC&0o)6)e5|`z5$_!xS^&*QkaZk~Ob@;n#R( zZxV^Kd*_ij^ArL`rM7Y|BUBcqVa+Qu)dF7wtbSf8$peR037tBa@W2y-EQ8geF`1$s zAvr7KsRnYt_q?Q8Z(*!3#MNYo=N-PyQdedRh%ktGyy?*YWDsMc5-mSGm@4;7Az#F$ z#RoIb+lOhxWs9D4{2pE2tg{VUB4-#Q4Gz^bdi_H2@ip1|9=hFoV7Ce|k( zg(&tE2l=Cs{T5(MBG$)Xg(W<|83?YUCClshEsqP>AzJvEanE%+ATE&I-|jz^OC@+l zJH!Ak^<+%C)(FIJLqnG8uAl(g8q|#x5Iov0zqL)wnUctm>xMNE`|C-6$oL^-6LVmW zS5OyDP(x;Jm!P{e|MM1B_K!1xx{}hkJx630THSm2!Q&Z6I<4dVEye z%i~Y|=DN36CO_12W+P-51QI05#=+>OMNSYU8TzUJhIhD9jS zx2`}+58Okd$088<)Q`7+xYn0G+px(sPB_6FEdOkC$-PHKJe*1V$9gTuld!mYBTWoc zp9f`P&($2R#9~>CUiX19Ql{~9Mi=+cK1?q=0*I{1z4S<~rbyT6D0WlUZ zAHKxQNW3w1adFWf4fGpo48S8Wz`Ks%Rj>f2e!vS5j&Nl(x`fW6Gc|>P0jQ3!J!J~$ z)1J_}f4Ah_e|HZ?Kd9*7T;eVZZLdz9h1Q`AyCksAV%0GOQY&}~l`j6x%;1JXuJ9&Z z-|XI#$`^e;Cr4qO^@`zOdAPk5utfBVx#c<5N^CS(c2UioSTPo&OAbsIld1UX6K+P_ zu-e`SGkB^cj&|iNpKosd5Zp4DF|UNY>aai@Xy6JOC_ezd#g8}t-9)A5FCZ2&Kxc}x z=CVQ0Ij|I%XU%x;>@UBIW?!!-^{Ak886i%mcdj`8M>5!U+atR+Qw( zg!FcQ`m#z(6u}yyNF=E}{&M)0U4u`>KzU`)V+JhuKr7#k<8xp>Yv%>J)EnvDxlB+r zs8xD^$(2lrkmmrkBaqoKsBjVLC~CspNH}4n-m^wCsUw#g_VWbd?_PwR=lDB~Xh?vD=>D(cU z)w$ICeo9<1a0ie)6{MrChT@Fj0{Z1k8KDA+qK{~k#}v7Qt~Md*_vg4i(_RB-2~a43 zXy8WIfC7ZAJBHvjogC{;81TZb{;YFATTt=Im>g8DP} zW~j|)wDiw95b7t=l&FSIg$7J^2AKF68kbAi?1@xc+S9fDb*Tk&pGjx&?rJ)$%0;r0 zTml)SZn=Ee56yjdWzS1E0Zi^NXi2PjZjJW066YWbX0RWH&>iKiB!ac(hNZ)+x7;r~PUS^)= z^SjZ*N#79UOoL5xw-t!z<>Q0aFmQpz!9LfX-^wQSrI~8seEpOCzRYwAp)A1+DqqCp zf<;wT`%K%T?;rbNl44uC*SfVg!_7@5@2aXBr1Y-0Q#W9t_9 z9kM=6#L!~0RGffI71S%^PEbB(Dlk_(#@|Z1EWmP>8gLDg--R#HFFzdz!tdx{{7B)o zH7wb{j?`5pbBrH#HouSN9X0u=5jVBsrg8m#F621)9G^)?89YY4_F7(rpGfH5FH6BkDl+2Xo96ieVeo`hr( zjB1w%@E(Yq&l}AR^#lq=5v%ssJjmE}dR9eS^WH@&SSkWN3(O2du#fRWc5ew5VJ@>q zPFcZzHEX2!#y@hd9$~7C`6BSnYG{elhNjCFho#e~^cJP9$XNNx6gRXC-fx&2)&Az& z6lwM#>uji6VucAkq(TtM8(0g2B|?(_Y;kcJ+^Ms_`ubqGDR|9l>G=N3(6Cws2l{-v zcKye2vB^v8#Chi-A=1uuHIq58BC2VLf=}5dW}ut}(JVwsAFfTeLBnr$18N_kemGIS z(6t6%R`^>b_JuYYsx_$iU*L3*JW+CqYR!vsocw3$kOT8bLb)AGiW^0+WC8%oZaD5% zl#CB7UaiEDXdLJIwg1JTdAuPl@t5xGyXu!v-(a~1l)HuAJOBW%S#Zd(C>imPK7g>7 zr`cY@*YCp85{xj|&XOrg7t?%z9|YK-WwhE6)JMc>!zs!ue#e}N*av7VYv6SZQ01Sm zzBYerZkgiI=~?rx>W_T|Oa8RwP-|IBWNv`J8!^t&fc8xM9;|rJ0PAx2g~AJV@zf<$ zP~bU+l?87oT0ajFO_8Bg}mT_g#qzrB|Y5zHxH|k z6W}!UL)&-T4hIC({`MsyZ2|*-ZeE_xYN*#$wOVfb*bWQNaS+}@GN|e3kU@zco}kQe z{eJ2k`-jGy!3K?=lm7c3a;&+PS-kF6ihdlqHtwYQ^kydCq|ya;=b1y@g6Oz&idRk+xAX;T{3CR^8~JJ+M!NpOx)8CI%=?9@_5_=zvkw3q}x z5esLfp`!d=ein(*_EU}xf2z7RM<0hR`4F+*Fe_@MY**~5Ck&T|=j&|8687o|nTvw5 zt`bfC22NzVZZNX>wVag`0$U)w4}e?x1#A-W8po(2Cn|)TsQVNQEZ)$-S9eSB0!i1s zWkvo%3kUyn;UefyA}U$i_Y#D&0S3mIpTlTsDXi1CPI(x&JhsxroYSjgpcqs7AzF09 z?&-tPzNS1=5^p3Q$!zW*B8MO z3Tj>gd7a4+8d)*M;TYOa$%zB-0)HPOL*`gFO5T+CXq&-1q;b!7Tc_kC)kT%7v^6%* zHvhQP4(pNIKR?O#-?V@%WtsfY2meqS=GV>Dpyr$>At7tJ;Z?I(J!rcL{v1lITdM1^ zjNXnsUIY;`Dv^D3oVkOOrPJ~?=$C=9fE@PkF@E3o6c{k$Td+-AxVXc*@i^*JH2VvE z4)uhPvJYKl`c&0ym~vcz3}^ZEc>EW_hNm~UgJ)A*jbH`-T$m#coKG!sLw{HhP+fL^ z$aIw+_TQ#KCm*YJ;UzxNE_I6|8f-5rZ-g+w5juqxxrfWa!)51UlAFQo%u5^;Ff)@% zW!m&BGV22B9W*_j`Czm0($KB45%kIzCV~HAi+CIDcv@%$++!cz5OKcS*c#K8Wt~{5 zRR|m-NIh)!*V@e1;5fgXL`Z!PL!G_Jdh`w}3EXNq3b|F4VuvY3jN5orurMua8am^-X{ zg8Jp8Ql+&2WQE~{9EyTS>qar#ZawH2!Yn#4fhY)00Rmy{+V^_X>jDS zB%2_Fp9u2mps0PM6gW$={7C-i`TrWso?MCT7U56oC^ujmr3GuVV=m2)SOQ+=vxzvq zHK~F`^X>~TB}be*%ExcB_{zO(4VaXcd)`HAwz>(n%f)zFZY~X$c>_dVI)^kbSwv)n zJ;HVv?ivqczr2Ngb=Y^L!+xD|FuP|uCA*O9!IM#($4kQU<;3eDukldpA>hyZ{>4SeggSO?Mv?gDIH-4O$Uq?7MV3Psg|J=V=00#44Cy_c>n&0(0~9|DJI& zgtxB%2jULq96KFZlf;DI1>0eG6H-i@-}Bs%(H&bm{!`l3^%?188cB?-)~6Y@$Xm#M zR|~YinQ8&+eXF-Ea3w-vuz{+aAbk2Z!OHtX{z7{!@4BZP68>j>knmRp{UBfKJ)r)l zr#sTDhgzdK!5s4XYhq4SVG`lX3a`p(q0k4S8P+n>`>gM913kiLSBU*?;FFlI1D+ig z)6?e-@r}UECOg4SeMv(+ENH^E@080YWC&Kd059fgtw2m6fmurlmFUV#e{BO3&s+#w~OF7xapF&Ab1Hrt1L5 zx^3S=AtNNp%7cm`l9k8^$tXm2Qj{Hv$eziHii9KzsU*o>g^)BTGpiC(S%o70=k5Le z?{R#`@g47w=lA^X>%Ok@I>(c+vlrODM#YMJi&i_(nn?4pgLh)mnb*{M;A_go#n(*F zlk0v2s%A-gYgxb3XW?efp8^iSp>YEZEj|<5s^BbmZXG$S^mJ^~1pK2kExv;KfEm)S zRi)qN#}2%{11lg@1N1?-TU&R(Q4|U*oqoCF?o(qlVb<1{5BRAEtFh~*$+LOLt*mT~ zjUm5vUe`Vwyk_slOTGLK3;(=l7_&8wmh`lwXFHHfpwL!&sn3Pu9CTaQ#mxBIktgq) z*{7(_uGS+#2ol5N;C`u9_k$|0rx#0v?M?I}L`n-VThc)34FqE~v6@+vtv#!y?rsXt zm^xWar!gJs>?Y3rmjX(KRcBp^y7vc_&fl<*7`Ys{C=_*SwJN?zEIqmTX3@jUHy;jY zvllYiOiDl%p-5lZjMw}MbYTN?cCkB*jY9=CDhEIVR6W5HvFd5i_P0N@{!!OgzoZ>} ztupesVvfsfe(lqYIgj0046{%YfyBO}C1>cE!q}aw)Bb$qBjNi|OhSkB8de=&#eXr@Z?>1-7Xj=S;_&59FK@0hpJ?9_NDY)KUR0IP2Q5RcilkVvfYC9rv8sb#XhXG z=_6qHfH190PyJx+?F$`n9kGoY2j=6&b7Zpp*3vI5M;-nHejO^v-D`dcd=x9txUdr= zeoojy*xaZzedY=LQ1J%|=&jKo4IPaaZD|?W=A`m>= zc5oYo{`rjJ16s^G5L?nLU^@AELXbSLVk%OZH89+wVQ%Mf@xwNKGBYoad_c*HFjoCb zDcO9mN+^ske6?2d;vLzIT9w<(`g7?yWer)q*i$;5u?&=tUS7>Crp9+gUBsX3cRzm6 zS=dUv6l`l^oc}xi=<%EW&$&bnQ@E|?Bh41dow@?9W{5xRdS$iuQ(hl2;Aod7SlBgs zH63A!`p)b7ci-{aF0i-qA7hV^a=+{$Q>tjCSBWTKHfbI&MTvV{O6#d`R`{ny)ViEK z7>QQJVPUf7u$or%BzUP8qm|`Oz*Z;nn|kx+dd16|mHErLiZ{w;GPnioc33A+Z*sKu z%B7pF@=L{>9%+vPDu;CTCBJ_2j9)L2ZQDSb z&A1BgOTkJC@b$B$=k3bhA3;C|^+=mLuY=3eaY^$%^92jnCsd!B^mN{2o%Oi8JaEpA zj#~cXR&Hy$L7()zJhT+pW-MZYtdq$Ew{J&S^6t}zswJ+GM|Pbk&Wg5IPfYYYWxP;3 z=dak|l;Q=Rk)WW07){S!Del?h)aMTRZsR>7{=?2F$mt7J^qS$Azay%wygpwNN~0Cu zFoxh@0$CYe2q7<%7ZZ7zjPHShb^m^y)7@1qg>jHf^%f`m*Ky5th|;qH!-AX2|9hxf#;Ho zpM?HG9`rpBkZUyCJHxI4N9fI*40+N&Hjw-1zM+9ZaN(Pwi%SrnMC;c|(ldq@KN3Da zzNuJ!&z83DMe%hXf5{K+RnN10u!_Q=w&qoqSlR^08n=KTYsDO38xx=hWFdTchAe;; zBu+A~l%o}#(H%Wl0M-GY!B3Ro|7k~eoKg=O4i+D>sZYgr41~I@nH7$hQOcjy*10HB z@_OFxkkXuN)smAUPH_r>>_-&^E+UqAicFzr&FpGq_JR?Fmzyn-#7F`xLJx%0*O) z)8G&1<$+mbY;4t8h3yns98A7T{!6Ca?~DZLd5EK|v-RqLWseUd8L;+QWFTV2S7(huD(H}1QYOGpx0k`fbtjGZG|{TcrT@)LVkkVx%P;-qG4Ct&tF2kf-;k+JePf9|KMjy2Mb0awp+ zUz#l5U2sUJYZc2JU59k*oWGyw+g09>(nQ!a)vwYpAggp@p+&St4^fN(_TB9czcYYe zR7+T&wndVOtx&{(S*vJ&d*POz4_8iFxzTJk>5(vbryQ;MA#luMKI zan#E6>tH1jVrK>}X?WmU3d0$P&e?k$vWC51{+Unj3p%9JzSr#Nkn4q8k!McPpQNMI zkG{*QGaE+L*uq?D6$k+%>_1l?CpWEXyzbTw8Vq}_aDNaDOmuWKW<85Rmchf?sjekU z$9RINMJH6=Lapp}+L;nYRKB;jBk-5zI0S@YAtig*qYQ-xMo1;Js)IiK2K2f_aF?jraf@Xe$Vb zzAK__V?zlEGhn@6mn*R8lrCNJ_nX;smnwY<%)@?=e&=1z;mz;f${sYhX1TK@M}#b! zM;2BlZP~R8q{E0u!C-`381kQ+A0xNQhU)pBF2fE%N2-1a>f1K6Yg8re-)2m{|5IH9>VOho8aG;E`fhP@{>7V!GafEv znC|IrU7znXyilDf0)dv)Z?@k7>if@~tJwOHPJZAdFk!a}p)!M`OI3eRbLHbe&CsA>CvKQZ1#U4|k65^niCGh=;+kTg=Zbx8$1nU$)rM!*L=9*?IjkYIFj1wTbXZ;VU z&N$Y#q%sJF2wXkMX;gdV#$!VFk3X>LqqOMU;9kieWAs0?bfu2(wf=sxKG-1zs$Vr4 z;mU>?#g1o!_$jlDD<^_xmDmn7SnMtY0P?MF{1Bq@sy2K54|R)Tp@;pH?(3(ED-oA=ZN2GSlGl)ZXi2awmm3QLTUJfa>2^7Bt+-^)*#9me0u zT6H9etkImkImynDa!R%Pr^E7!V-@(F^SxokwV4n5PvS>tS*(=ISJGFELC?4W?>=J( zi=ip22=@>H_3_JP?|kOmHp9FJPQY-+|ysOxSQV*$g@E-$N^Ll<&we4ljp^Khf-@l39isPX_S4PZWzuZ%6Vfk zZDI;VsWO~AaYYs~9s^HLY;9{SvjsKl*%OV-Alc2=LSrwaP^xUO343m*a)Ti>F4mmf! zmbU_qswFvuppym$}P=xQ+mEXM4|+DeEjNL zAWog%Gv_ZFG@~Tg_SOE^hj{-Wo;|gnQrUj1MH0vn6F`K`@p~|?hFP4tAk0?b<%QQo z9v%hgO^?v;!kW_0dJC!E#VbSsc^jpeZ~IcRWgJ7_C)mF?5FMzaxLkAVdP46>-P^uo zxDf___{8}I1#f36AtrT5$;*S5fc?_Oh@tYIP_F! z8ay7o*bj_L~x*9`c<92DnH344S4@WZM4R@JGj>Y|5RHl3!WST@#<_vi?)Z&xD zWSA8sE<4h@_1gVYQ>vtNitB_z&F9!x^L5kw;rE>>=sj`hN6Oc~mCx@h$I3>G=MH^+ zC(Yas20VQR45Z4?FcTI&5I87Dny(#yGjq-g#?#O&!Ak8^0tNBS# zU+bOZ>)A@aaOvH#?}3`-_gu%@dVKjhKz zdkYNgCPUDFwhS$Wy}X03S=q(HV`GlC&PAH1_65}QEu9@WKkK1`ZokPvVscKF8vr>5=b7<$ALLf4IB<&y3Iz@07K)ar~;&*Wvi^rHzFm+@ z8|@m3K7J{F@S^|qiJ>nSHM#m7Mr>+SWW@ZO;@hF|c+xF)tzf)7FYjA9Kkcf_3?Swq zd-yN+7WH!w`3sI~ySFX(^0~iUMwm<+*PAFMn$yt8O3r zMN~{6-97!aNUF2T%KkPwYrI_dZ}`(;AO7^g^EyYW-mTdmQuFlDnRyR&x^E#m)2r9+ z!+x~UJ-$0S$_!ogK||K#qRGv$tVlR=id-~ zj+Mm1l4V`YL6Bhc3Rc2?7EpT9L<`x3PcxA5s^L=OXJWOkk=s62%{W-8Q=yq~e?f|0 z)Lec4>$3q8!9_x|Mz)MPT5qBEd6^r9+b~u$!7gYk*z&qtcSE`Y9O%uABE9pQ8EHKW z*j@q3o$5oWjL6_S-a}#<1RRE5r)MqEN&(B)}MLj+2`4Z0i?=rf%J}X%>RA04JRUO zA|v7lA?sWS*q+F)rm}H=<)7-yn{9mfVv2efDrWE8o%@yVW&`3yaqWpm(;F3#oALg` z2S^_W=BAEy*KJLcZ|P>3EJ2JEb^<&9Pp+a863UYkyGf-_ ze>bf}1wWhv?b1fs8<-Ezu-59+>zf(gC<yeb*T3Z_{5YnH-+r^5#-h zGWy3H0^e3sv7;s?IS$-+9Ejy5$4H|4lX#zbgO^sK?}v@FH0YY?Nc$qj3Eo0^K4wVx z(1GaOGW^y-7F&c1I_V>K+sE)e^cLTK;c0cAmoBvX$Ci@px`9#4{PD~qT;*RucIYjB ztSh9fZ)RoodPBY9sdYy#hEtWf2|O0liJ%JOIu9rN0;aGMTr17C*i7Tb@wECGk)_%Z zu`X`Hz_hESBy!D-hYse@P{0rxrq!2}jE#T$*8qe5a4MBFPe=a$sED;V+NO29f3H&0 z{NT7^iq5@{9$~Q`kIHj}t<6N4PYek>?0Qz{Oqqkkma=uylHVMhh>musX#XYELbWE< z^QwQ<2C>7zZG*qS(14K;;4WLK8`^g>{$-rPbxwjQ04;&Ci{%Gg8@V_*1nzM5Z-pa# z%G;6%8>oMAXPcTLMdR)`YKxl?{2s-eBvwDJ>#Xj*aMH%J)BaZ9i&u2F4;fV`ZC%3b z4Akd(OthLXwX4R*OsOt7E!XqN{f*QJeVeEhr5^ebQv77Pv4Kyaxw*MG6}~S{>`E?A zR?=&?b)(%^t@*VqZReP{T-4T}js^`rXwEQ=G7s$D>-F6}JoOfNJfSO(CyluGz*2O> zZT7cOnGlr}{M)f#x^8>!u)R!;!S-FW{B=Ui%i0Fl{6(92IF;L0y-+>QBzWPdwoFUm zmjv;awILbB#a@6M!PNyFwH2!nc%{OmGT78UtnZ~}tJJ8;h3C0d9N@*y{rs`81bEeV z?x&TJk+egbaJw}Af2@clB@7L9o*$LSpJX_qd+sB+`EwE25X>lykTWvF={V7{7V%>C5Y?|huEP4m6 z?s^$Tsv8`A_TEF3<^s2SX?BIuw%hz#KtO||sq)a&R(1wTJt&w&sR@%{i2v`vgNu<_ zUoX-&sp0Au{b2$NvOw$! zPgnuPgX_=(*W=hi;J?40pa0_YD&M1;WzlEM3zJ9Ou1N*NG@JmE0ViLQMKawagjxS> zjoi&cErrHi1D;lXg@wtMLYsKw?lcym&-(-EBKpF32@F zC=Z+yewrcz2SifkBU?p^Q~vd?-R3AP)F8J$XwIF=#wto2(Jezw+9r8wuGc=a>wigO2=SC6tBtBMtHZ*t z>A9l!vFqP&ad($hUxG zA@d~>e-;b)r7)|=N%PQ#^_t=v+$EEkb2hTvW^%fM`vPMG6~gTJFE9eDMMdmVZ#at&7B(-J3wz_*MV@fCCpO!DkvwPh2K3@c008h% z^Mhj&tdpOEeS=Q9zWM4lxreJdehD#JbA5g0o(6}fO;X~Y+Y<5!@^wy@v}_Mm75UK` z?8@vy+aq-&j6bw3sPsyk-GGc?S1%LY#)&a{n-YE3U}}cF4kreLr_#S|xVYZmeG=V8 zwQAv!`!U*y`47H((Ud4AXl1DEdI7uf&r`5A0x#X*nyJ_&&Nan+W6JsiSULS=UAyOoE+;2vR6Jjw z$jg#3cFnBAYdfPayp|7nznlN>PXCB%#$S}`FtQ~KbZ9;5VQ! zMDc>WFUc6zgvse2YLWl>v|esx%VufZ#~u60DV*j+g+mIFxwZsO5Lj3CzXgsy#}@v} zb$wUz&R|Wi$|@kXo~`n&S}UBKt~ZuWY$~H$!vap>IX0df-ugL1H^W(_8cN{xe6`s- z-Ne!|M%Y-xfK%ewA_55fVF_X1wIfXB;xWr;rjW#Uue(ewEr}BllE@yweFiyaW4Hj& z{RsGy(RJd=ZU-uL4RYEs^lWx)Bj>OaEWL zVHj~kcWG!{pf^dofJGp(FJO;dhVPQKYBc`Zp&XEvA&U@Z&s86mXL%kv*SUR8@y*;n zd+F_5T!S~@U!#3jtOXmZ5MEFn;<5(i@!4u4DCEwY4miZ2V1Hzt=>|y%+8$E48dXlOOp7Z(bo>hQ{ z#Lt(kT;?}A?B!5R)FSa9ok3djw!O&E4W~FzSQ*O_4F5$$Oa92(%;V6eJX^a|$k*kb zvi-+o#q&l{v=Sfgb43YX{O4Fyp75;MTc{}Mnx@u%h-k5sz!ACdvup&882fR}$J-@2 zWYwNh*CsO;;B!(hOWi-OVc~_fP}HXv9?$C{^3KJ$vzna-^GKA@*chs>>Iv*t*Qicn zMy_05S|BA0-lFI?-AtLNe4Yy$m9|K?3eaO%CqXY5MjwGcTS{9eIp~spTs zL#~p)h@r`T#m{$28-bt@M|33SfH%LFky8w-`-TrgR#BOwvntd{w|gDMv4L&0fCqMq->;FJp43i*U(z=mBa6XYT1qDzTOnbbUym@Sz$YON2XB zD;UR?t%uU)Q3bS8A&IoK^aHH`d^*Ls{V~n2j32+Do*zuubfr}=$S!uvRBLXZw_-mc zU0`skmFpQRX1t|hEUd+jQncaV9koa}^PK3f^jTh-t633)MA~S65;2m_bPN^1cC-=y zI}AD-63O5a6{jkCJcjn0$n_RgWt|+hPkP0-8t8(LG;Lv^-+kjXbMwU+KsPAL$T^wE zv6u%Av--RoJ@qe@5GKeThq_Vja0Llz#*_|4@Zd*oG>dPRZiPor*BJ!BUDC+C=`nw{ zyFG_yjB$B4M$OL7&rS5Z!4$x`B;haC@XbOSV;OBp{npb;+WYssJlrPWO@A0p0gwv& zz{01m;(X&B8&9gKZ<#iaRiw={6B->Z^ftX5Ngr|PEB}o+8(-2BNmwso|Au?*M3KD7 z{MTzvoZx=^gD!1`h!ltM0Xb7AgZ41eGDdv;oZNn~6Q|q(+*=l{G3RxJB zvu)RQ-l$2MvvXr1)eqk4=v!-j4U61xd+@Yw_=peuvb}-P55Rzxwp5LefpUCVEj)9} z1e7G%2-^WG2`(okrKP6Q&0a4UN52#VFkWxE;-O%9L-%hjSf7Pr*McIXaYR-k7`Vt2 zMCb*{7FoA%p2~f~UP^d``Q;wY=Jb&D;!4a|Xrs^6eZZ-B`W2BfXGlxx7SOwc+`S4I`AnQt9wF1~Dg}*r5|96`lo@*Z47Etddoa{n0-!FVrj}0_6?aNR-kEgzj2b zs$eS=id`cV*Qo~&mW6#wzIyDjPd=yc501uD5U3rzYgp{EKkH?>VQw8loTL!W0)x3U&ki%B zTO~AHyq#$i_VR3V#-~Ze7LSijq2Ury^0sDa2fwll6>JKA)+QIR7@8Sz4Cp4-s8cx7 ze+c%BIWY6~CMmBMGGrW0B0@bdQha}ZTiv&V@1)rk$AQ%a6AM$`mBq!f{Fmue^^v@2 zXb6%Vtk!N0;MBrkvHmZ0Tnw$bX|=pJkUY#fuVBk{9C98(qc^(8j=z1|XTtfVa%L#%~VYhWRvsTVEFgt&_M$_3)7U$!n~_utL~0p(9>(hltfI`mT>E< z+sc4kyynvouHF^jdd%l3Qg+C9)yruwuwi3kU9#l9OWP9Z{!ZV`LRz}%so4GM>V2Hi z5P>Pe*9p3(oi9QgR=*R_bKet{$pou&Tuf;So{q#vNQ?TXrbEO2jEHd$8*|$~KeM+~ zJnwGucvU@7r^|9ZC*op^+&tKaEg+KT2i*GXkdnc4qH$wgnO?`NuOIC zGt7k&>x2@T2+I;{0~PbXrERbuoWt>!kAJjlOoTjmh23;r;YeqkWmL@AZ`Ldh7Y#VD zr5D)S8*Znuo4Xlm+2mw?^4^x6H3|DJ(lCfdb9FI$psfOcI)L&8e{gIrb%sSZID_j1 zYndLBh4cu^B+&oeb>AyQWbIV*x1W0-WRh<1_eL()ma74~z7`a4sAW84Db8{++!>bb zVBbXhbQ3MSot{CsH;G)`Kny=n68XZ3B4bTL`2(_P+fZ&xB822Vy47LYII6E7MBlG2 z=ze@X%r?O1@k1jvA zjatLD!+6Wz?LQ7~Jb48^cn4@0x%iUR>uz5D%|@Y(aWbnZ&&?erUpjm!N!%DVt~Ag4 zXB6+<+O_@7%-7d9eKGYsGJm@E^B=TK0ob6m%$lgt69>1=7*<>Qz7YhQXT?1CcQUSM zP!q+trHxnk9?3FJNvzN3{9#?*&EsW#x2tKc>wI0h<)_J6%@D-6g60h++UjyfmYmP> zk%mcsa?xhYBjjNM1-(u`k=vNlv>MhvuDB&(o7`OMu*tO7@I8tWiBL8l76;}86O$FbW)kL9xdH<2&TZA40FI2gdN z8{JdcF!v8L;qCL$y3EWIZ`*#~S{I?jkUlv(>D1^BiC6-ISnAJ=nB5%J`Qv~WU|K1$ zyf~r^N!xSw`hD@cfO#QAWQw?Pf`PnQ$veZ*_WQZil#5CvFH#4LX$&MfcMSmzgxm4G zZuH_EL37ZJQ5vmU{O0k6eT#-gqEVVzBEM~Wy~H>}>D4`??qm>)hOgw{oxH3B3nqn| zFL>1qgqq7rOGzj5o4osod=xmivAfgWDdt9q9Ol zcX9^J1w2#bZyu_PKd{Z#s&m`nbO;cL%|Xd8eKP(OGwjd6FnRzM!Z8BjJ@AypSjNtr z7;KyG&}&o~+coEY;tjkS0iQw-Mf?R~SgDP(VH!lDlTRavYxk*F-e{kliG3ovm9lOk z)Y62_CoWS`xnZWPc3q(U*YmwfRL_cQ)Msl~k?^J^)Jh-RwVOdvz^mA_RhOPwMR*Yn zvZeGPApT zS~Xrav2<^Mg$A04JaxfUv9a^E57fW=OQkyTvrt7Y;&i^cr5uRg8GM>HY3B3vu4^8w z_D-KeNiZ=oYHF@O_Z#m%h$<`6k#nb3=JZBj2=tFB2F6c;^_9hmxPFw)x|tdde2MlK zOcqT|leO=@%_Kqu_cR3K+F`wlw<6qTW0D<4Im&3~OeK&Zq9i9mH~14xdI^u?n*AOm z^2tjb9XF9alYBe0TI9@rmVa!jEBzoQ!{G;5#-uEO^uJu}QUM1vyIan^`T6-tyWY@K zAyu#G*ijBUa53-|0Fjh1G6uNZbT49HXV^yUiwIZDdil3Cbh;wa;r>i@LFdBkde-D{ zgzCb;OM`pYiF&xnctXgZhZwwC{WAFd@?REuvuTI6$SAC4aNZE)ko+HSX_{e$lSwci zyb88=@7qh0=8tT1VZIdUTDqP+Uejqj*?Hmhs<6|bncxV3!yuv!bP`6w+Y_~O+KFck z>^e6<+LR6(qicv@%z^&~4lv;~bI>K}#e7LS-kf1D>TR(`rf9G*bNv}* zZozCW6USaJBs*Y`yu3VrnE+@wnz)IK?@ji5D;@*ou4Nd-<}JGq92UTr=&ttP8>MVh zZhIyNM3`B`AF79oQ^K6Z%vpTOR9&IFnpT%oP7)4u080k|>VCI@PFnl8wZ2;TJ@+cE^4#Wje8`XRW{z z=R-?$m@vj>V^V~q3JstVrjRAobMOfm$5aE(%WnV%kjJfRz7Ga$%B#_KnqEr-j;_=n z)apM3_2jO%K;D4DNuXnl)5JG+=*<2mhm?r1##7FhBI;>=F~q}i0RBP?j1 z?KbD$co%!$s}nc6Ol+-A(E9F;yGoLLUV$gH4E2T;Oswe7Xr8x^H0&)V7OB0i6%TgnJd&Owjx{Td6UhW76DJ(0-9#9E1{qN+>5u$c;$! zmay9T=d?*L&R9eTauP>X%6XO95Va9W+tp4fvvr%7+`x@NuJyG4W|loib^Qh&-Mi<5 zTK+%A@RALWFnIjaASyz|iAdO%*GlN$0LLR!J&advYk_V9Q^hl|b573M>ceZJSGLg9 z?ohUUQ)jqM1(X36u4Q(3_vmdCyHk_fT%B31~GPnES2mT4XNH1A8nt;xGSgP5kd zc1IN0PHw*Bm0hR2SU@;g3UN=|hMq%_!PQVC*;|;^hN%OvlR=%NZG=x_#@GecQ*bzJ zm&U(836%QRk6teKuaG_@+1<|ULjDuiPRrzpA%csh^G2)*nrKDQJ&8M=89tdBJnG9^|c4=(<;0UTNxT2yaO&)OnmN^Zq zJ1IU{?Mfh%Panb*Hu*hH9zZLq4UkK}fipF2a!k%}z=7G1o}M0f+m6=Q_u?NDVFDbS z>0xU-y*r$B%6k#e7tE6iS*f?g6&%i?{&3r%j^Pg;koOQtG{G6j<=V&bLHn!nG@yMTyTqWlp#FWhb! z&gx=T^1XOn0mo!{e%7XSg?<1W(CLt05I~^1AX}ZJbFb$8bQtauzY!(acCyw-$L-71 z>#%V&kTlUs!(dE8wAng6$?s1J0`f3rCA@+!C3G`JT8aK#zDGpXM#sU!}+f=igGOe^-<l5SlF7Y{0FAgDm?YPL8-pRLEF^Yx>AgBMSrP5v=bCtttoh;yT;@8+K7EKX9wKmFDFENG8s@(@JjS z>RMN@7Sv~B;z);{&b}9pYn#4Xzppmr7)Gpf$ir=F`y6{8ELU!mG-{WIQ^Ut%@CLvg zt;k;>=nH(@j5t>Ra~SmG+OR=ss(w9Q{q4LEpJLq`DiGi2KkcvjaP2n}$|bqfRv3wx zC9w)#Z6+`ji41z`xS^bv0l__R2!f5Ke$m7NGI>b!i$MC6nE=(s*1HNXrnQgoHmcfs z!*ZE_l~Fz`{OQo^Bex7d()t~GxsSkZ1>Oc59Uu7EeECnzbHA-DJs$^U8+`~Sq{^Zq zjz@Mw_tD69e)!2j`taLkn!*jnNaCs^IZQN&(C|J$& zHXama=j&Z{)c8Z8*n@`|Kd+DYxHVXH{sWAo=>PZJgPaU~&cZBWH0O8uCZRC6tTfgy zG5DURsDlWCy>_uT2&Z2D{{Da(ipD}%3y*fV?xnf7#ZE}jH^b+Vh@UP~m$Y8HGyp)q zG6HI2w*}uu$a)G)ApL9#hf!A5hvhL6?eIHzpwnavE@^-5PvWD%xn z<2_HESZVcZM&rW-s44XjidN6uoM>wMG@2GhvqqI_sMFw?F?@I$sA&~Ho0zhAqsPT8 zvt@>b)v+2hKSJ5ZZv4&MBpIGMRty2?blb&XWqo8XwUqzBK&8)a#}7X{Z^w6Z6|pCN z?7w!jwAN_*Y8nz!1WNOM3n2{T{ofasfNq=zH&$@`>4t69Rw2_382j)V{zY~DxlsLC ze){XPzgjR9(v$1spk4LXVOZ_>CEEK;H<@+ASu+a?CSh(f-gcMq=q6;0iHUKjRbtV( zunmvjQLuY?4gKPE$*{eS4xea_OT;Nr-}WDq%X6cz-`pj`?0;^xzs6%ysZUemsdQS69?1>-@nGnccY zId0OKt!3O-Fye#5G-GgpFPzn0uf0HJ5~#sf-#d70TI0{QL%gEs28^G;6FjocDlFRy zToyo|z$z{C(x%e6tl=GWrDL}XR$HA$;^i$Qf&#-tz(p|+6*bi8j@wg^mn{fKO3|*&mE8J@D6&AAdIazgLZP#7|mbfdn z`m85MZCZE-&os>WCy#*IOAqTRazHGB*PF^c`%AqzH1O}|dAQLPK2CZSd!;fZe#-QTttI+T^z49oUbfTnuYO?^Lz+&{a16!CYY^%wEQO-5`jXlwt0hjCh-Od znI&F?;*$enE#F?f{<81W!aj3Np#t8Q#qd9cM>5(tu+Cw?$TtE%h1;nE|m8+w-0)d%#nQokk~;C~j4fkPaM zsxV#wBEj;>0`tjRj|tN;U9H)VPebJ`4Z!d8BZ(pWMq@I^LpqRh6R((-+*A& z`(MVELh+5Xp2)Ia?31b>rf&lDneTE(eDvL++`_1)yj@XsG$nhi{3j3}_&33TRD-8! z%^Aky;V?esKTr#3DW8<98N!=+C@7b||c=7laA3qp;cV1n(1H z6u5}=xSc}xKo*IK9Lt@k^S3p3^`}p)yX`NKy7BfK@qkL83}kJ99@IYNt2~Pr1eY!N zrM<8bshe(~2HXFpTV9>hj#A%U3=uYqje(1eSkYqwp3+#a2C}~oMCACy%lHGxo!r9< z+OC$xqkOXzCZk24*Wz8I*q*4+||tb74|SMmvS<2pc-~e z9lp~#T8c*|A4kl$vJLMMsDUKKf=sZbXz{oI=HW*`@M`9&CZ5hNdYyCbP2Y2NYi1Tw zIk~+t^T1)0Ao-h&eA&;1?gs@B&=AR{kgB2jaSdL;FT82hyjnmK-t0XXj1}4_U{F&4qB8FMI^hAE}Lq@W(r*g6-_&`c9s#JqH%Hc;h! zpQlOnfdljohgKfnK>#os`(N%{%mv9}+tq`c?us$~;jeY<+c(SHKd}Fx-$02N<4y1Q z;R-N2jPD;MElMc=jN$}F)=KP0yCzDy-${#KZT^V+yJ@MxDy*lc=I@6`ZSbMio9vb` zp;e`$5^uy5BIo`|-T7H3TwC%jN_Gxp2?;9?Dhs^7Q(L(8(*eY`=VM7Q2s@Tfklm02Bs4 zKKgvz^0|czWD5i0R>r^H?@*n}$2p{sbWy;LJGl43v(d7XU`L=y1wXxX$Z0uZWc%XD z?TN^TBklZu<4wq>-FjySYlDHkHO(DoXx@#-8P4>gCqCO@ezP{cJLDwv6ny^_4FlfB zV51jvI*?K%v9`S!ijt=YzX1%cQGRyPxAT#;L|E-^x?&-v`P#Ueoa$$EG%O)MrRYWA z#6dQ-*d6FUFvUD_N;XyLf2NZ4?}pDN?G^Nv+@SUm7J?Br0tUP{gRxKn+n#cM{L8lz z(b7F+kA`{qhBeAnv1?z-#BH`Kfu~-liSk?mQqpW&*ew=v0narUxCJ>krhBCH(5lr* zzNMM~*!60zi-9u0vhu9=fB}ejvK^E#qf2D@jRACMO?1SloteI4V7JS_VCcD^yHOpU ze4^Q^tmGu3jB3#E*GUpzzR%cp?E|1Z$j{p~-<+ z-jm0@Q*XYdb$u^q;oA5x`4D6?<)|ChIGKLG3r4V4B#Zq2P1XnM1Oib`;3 zsK#`I!HO1EFd_c&X}V&gZMViOA_eFb-$bhmqACL1r*xeX z?8(pRzKW3BmFo2q-50ib;o-~8#IFI}8#srxv3FTt==zO{F18=v*&#j$uL`scgUr)z`}Ya!MLS1-uM39AT%7#JfwV!aY}H$Zb& zG^}>^;S~h#*obJ)^79q*nCdp@tyPGaxiBib-h=AxDpw`2Jqz)Yf`f-?2R6kVuM-;1 zOm!YzdIlmka-^}5f=lgqUxnPa3f4BtI}$>LsZcwB&$;&)_8jo}nYt6VFW)8Ja_9Bd zQ@|)(7SQ2`$Oi`Vf6)A(i_dzDB?c ze%3oEbULiG*$!(dIK-=d?Sip`JEoGarb$=}BkK*D!A^KOY2VdQ{MGm_=q8iq?1 zs;u=VcImahwbe;ldj5HD8=J=dCj&}++WWd2KToxz@5ZL9f#Xm0E#0knBs2aG!+6CC z1a!#0mUt;@l zij8S|kkVta(hcT>=ShqP{OW1D!0qHk%zm+dVTQ9zcxI}~FowMZ>t!(+#e0bz!L!LJ+kp;K=%DbuZGz3=PVt$~1Uc29o;K^idxA&o4F^SjlqI}<3nYx#&svE>*MNzZKo5qab9i@T(w~O*ktdc7iVA zY_y#by= zGTcSYdopNZI1>Q4J$A9sy!BkV6!}TvtWD|-b1B@VTWYO>Q-$e?j&>N6+!ju4R+$2j zTJR=86P!=;g-Wiis!61aq(t4V?#pmmub1>qmjgi+rtQqU@&&j`K=H$RUcYjFJeI0zQbFu!H&Y!_3~*V% z*J1eEhx>9aJz*Rh+okRFi$;9#o?rNS<8&OnHPCzQV!vejq+56E5lc&QtV`y?_o(#< z`3X3!ajp3`i`+)q50Mdz$2O`Knr_Z)0saVNI)a5y0rWLw1euDhd6$L+}@&u`g0A;V{Y*{^m&W(4t;3a0Q^^PQA z&w$cXC{Ou*v*#-c1Gx1Zewi3w+LfP2csvN`q8?+ki5X;R`>r(>eYzcoEE88Uu*PeF zVG#5J_r%TAxk}sZ^D>8Gh>*Zpe0VfL*gPIs$qc6aHHg+Xd!WR(c0knZh9B(XmO*8? zxLgVA5*Q8wYEZ`4Nrbv|4Fq?vw5)^+B-VrAPqd00j$A$wzdESq4R%3_vt1_Jts3mt<)~MhZE$3V$e1*HhyUO_!AoR1t5{wz98}u{uL zp&YOn$tDCRAJD07HfBD{P^lF@aU1T<>Vj9d9R_jpKYa`WE6tul8e$B}a^Ca9yKUse z*e^Y>Ec3vF83BD71tV?ZkH8ldk)1n_!h48qd@qm=w2oNFcl_2+ckf+QW`iGfb&7jl zfj~cK&dT#!k`$%7DFgd|>Njh5#pI!N&9Q#*YzLy)5bt9v_f&aaW@i@n zd2t5&1*A*QVWE3J55O$QJoo^>h6wi!VWjkwS$21UiTz@m<>TN;vDvR>lJA@qOq9Pa zcrffS#j$~qvJ(q{e*@UTdX(`xCaC?*jQEJ8`a>8H@%6TRb8ixepJFSd6-u(3l2N_G zf1z)z+%@Ht!p(f5Iy(!3D%Nv-xSDY7=%3(jR1Ig;Fz9$>t&AvHbOs0+7%dOL@RRY5 z4emy_srISh-S?tRMW}=Y5Ph{y`S)xI|2B%Ze8U4*u?5G(W&J&;k z3`!*4@W4Y(p&ADp6S~T=6F&BvFK@Mwk^M}c%D?-13kYuzf-+E~_38$~1n6O}8AB)b zOSX=yauc1<-j2aC?W1nkZlODSB;Jw&SEgUek3Vh|w@>~sfAT~B{t4S=q(4IrYoi2b zLQXN$puw|?v*1nq+(;QMy$&^;JC+ejaMzc-&Y=P7W&Sws^C|e9^B|U(@ZJdAXcm{u zX|_{Gx`-tyS|;)cf^Owb$RJHrGYdI92JYqz=Is}K@3XgckZX!{M+v?_vIWtal3}Vt zZdp1ZoFLbz2k3Bg-j4aJU89eRbhG34J^X$(Y=UK}{3Y+nKO4}*o=4XFWe6Pb1O*iJ zkDmRd_~^ld?_>ss%QRexZV@I@>^DVa%z1^U`_VRU-<#BjsXhK>+~nUG?9Ol;1 z{-HjcM0iX%eq^wtc~dd3V-8Lb3Q_E4|*|Gi+?cmGPrQQ+FonxGtN!_3{6eD zu<8U<(lT6rej?zdXbzmDs{bRWn^ybniPC7{f36m3D^&&-x~H{b+dYGT#l|QwY~MlCAp(`!E^oD(X*vYCe`? zQtVRKa^*=*-u6!t&o9pxZdyh8!E&wM7jQixU=3Y<^6GQAi91RaCY^WqY`JBT2TH$c z7pXq=l7Dpo*27>g0P6mQDwosl(CLXNRL!jX@qJht;Y35z=$lnC;MME0PH_^{Ef7}q zV&kf2}v2kC`{g(+3e6MJiv(xHH4%M+-_uo2G=-Zff&^Xj?wLl5ZC&hahMi7wz#T@%r{;aG zC3t60U^&({fP>S~by_DZa<_(>8ubR9;P2m^wdMsYo##I<|H{2HdeiA?Pg|Q|*ydb0 zZQ(2R_4TN5ZL-U>KQ=K?UsCr#JAeiYEdAE8bJI6ppqspjWgbXC1DF7cP(*TR_TGGb zT+&{0V-v5{w^fuxJAKB6ag68ib4an>L9gcH;{*DTpWou$CEus!KOk|y?XBI+QDzyp zapQ8~MH7_oT$%iaog6*xF;)7#3W;?U? zy8)2KLMLUjyqomKKsFz1j8MZ<{&!g~#+7I~$-pvP_2r{SKcT)|6#DlhEa0b7Z9acS zvn=|#+qaXnbIeNmPzD>Bn*L;qvaSgWsyAi5=?`@{uZ%+jHviw?BW=x1jIwPz!Ebbh z@h*=?)ARlCAE#S%xT2D&pVuf9x)D%+tB^^{Te>1_4ll@G&lUGB{5iIR>GHM(~bu2h|~^TzdVqhpbN# z2~nn+@p((UOIX+T8KKHxY;ievj&MmAnC}VK$~W`wY;SEvOh^zM7>77%r|(flYK!x@ zR(~V^V{-Vb{9#%dvsydpn+=T)UCwgVom(Uw^y*DNv=8QsP6^lns5(k4+%>HTcn4E}4)!gT0EwNS@8fjHA zF`T{Xb`>0J?15ig6%b4M1X3GDzd!zJkk1ucI$HD6QBJDU&M^jb^d?8%WriU%--soi zhWt9+q;!Hl=`yWTDaO+tJb1A&Y!4)kTc}=FR#y1vDQeU`Uz|ox+t6=xBK~!DajnZu zG|~!xVma`UmTzhX)K&-#!NtWzk+)25d+&`m6!#L-Vyc;{bY2Xev9+arc<{%P zzZ$`^Wown7uXA{N<$7+f`x;I&AZn&1efX4GKKh9Yb^84ZP99j@riCnf_qbcu)8=aN z*JujLN~FGLKWM|N>=hzeCP-Ohk`Q$71_umYpWeT3vd}5BDzTLKJl^eIJwkm#B^!4e z3bOp!4o=d+JF@!BUwczxrUYlk;}Hcp-LBtW zT)F`PDPS7BRoy~~RO%<4=bJVYgYP%|&0AWK%&>{1h7X!^P#)yrL4Je5g2(9AF z%VI4>TuvzWKv4WeEHU&OMSboU6?s&wWagQN{B}qwSEAi+t>cGj1$+?g0mmmvhIP70 zlyIe`M9{`)G|%?USVnKzu^R`7_?0{};Bi#Rgn7@Zgzca3PjSTyUrZ!YANRS}*$p@F zfA?HR^Qx1RWLo&trcB(NE1?a5Kkyb=R%$I8;-1c>elzh2FXdkMD-mn?kEM@1F_%dF zwHA=qZRjms;fs_gS+*!=j}+r`$l+7H@@sWxrSsCztN%*+kO9%WciA7pg#S>#6NaX9 zZ@0uTdOwPZi$hx?Ec{G6M;W_gXIuV~@y-XcsX#U-($J8enfz87a*EZ67-@>c36opD zK86NY0=Url(i*`)S?8+29jQ(H9Meay!E6Zgo-dAI!0B_n7@8>d#DkfpP_U`-Cz==MYqNi$i74$7s)gAWR~+SG3rfeFvlI;b>Wd$S zovpnVOhtpuNhIhgi5bL@rQ&3A{@A@*-aiXZ?vZovS<9^Al9*{#efseKIL(;^gyd~@ zcFSxPAEX0`~=06njA*C;%g-#uU0N}+D@ zeLdv&dI*!Jnl!^@Nw!l`QdU=25v)xz9GCPUGCCvq_FYOQ;au;-6b&fn>B+;bN}&HO z1%CMKS+C1eEv`uKy?jI>fwH&WI#y{0!CYOA<_Jd*B=J!1S&6gSAm#q^^+g%HA^0}{ z*}Exl15~3UCmqqli^Bd%@0^#GmH^&HPHE|gjQ-6quy+O(2WLHbH+><^`#M*$f>UYnvdUq` z1H6#cP}C(D=RUcJn+KAaUoLTrwqX? zpM^PV%uk<=V-L9B+j=Buk%Wr1rJ?4^XsI-AN!AB!-bN$HP?zVV1pim{(6cV!0e3fg&LguBRTy8O4teBS)EqODh zvoldg;&2)FQZ}n33~|Bwmkln3A_7Ghe6m(0eGBV(4RIN)$rQ1#in?7jP``it_z_vc zEv>Cpi{r;J-N*R}uqK2BUS<`FV-yPW!fS@Kp!3tGvhwn%L!t>^C|StEBh^%VTq?DI zCHTAy#LPLKZbA;`Lcy*bf(DA4{#(IeTUtUuA~ zEn3SND@ZA>4{G3l``ACY|oLr1#2K}A1747)-358*Ipr1}vo0YJZd zfI~MT=8e9Ghot+xc-hsuL67H9T(`97nycN^cPiG@`4AC#p61=;GWv*W*X}Utrin)l z&oe5X49ENVsN=v>WW$G?Vksa z7JS~=@MmX_jB>-5_M8(@XJ{Cws$i;iOsMpFxaXKcE=j& z{v~?!!wnH)V)=>Xzrk-B9b~u@lEw?YSqB(yJX;L7HYQMa1A~&t!W&U@ z|Fqi@v>O4Vb2`V&wc3VOv*`|rfb4@$FR(0jHdm4>FxVAa$~wUC+~=z&Zl%^!{@CwC zJInV@^2J~sPHiNXVB8u4-mu5zWo5k~{^=!$=EYHqv4NZb(>(-E61Dd1TE^yGTo)2G zyb?qa1S2D4A~sOj<=R*DAN7g1%R0$Dez#HX^CUIS;OQ=Gl&L;SPboW)Yx4yR1kBEC_p<-+^7kh_2y*G(&98 zsIMN6#zdbOZf>qj74yid86DT|&xwhRzrnY)h1h*b zrzn0f$LZb^A)O5PE)O^C!ZEk$)_4GiM2%VQc4XqPx{JDr~e~+A>ot*Ly zK8Xh=r{UM{-(n*p?v%_AIAQc?qT^iP^_ZIcDu+gre{bMTTUdud>@n1j$vOSA_`*46 zxR5?(s`fipDf7}}7AeL8$fb9Tw*92AcDyGeDIch3_fX<3vD7JXRus#Nzq^JW^|_-% zu(iz)q;x$W&SgL!Cn3zbu3Z?FSaqT$s*5zky7&5G)A-WdkGgHhb!_Vbs)szIhpuBo zRvmbze`KxKA|Z$i4T7^RJ=+NSzbsimCsnAUL#OnkNRJ1#l#BXuQIZMrI8|B8+~W1I zsyr3g$S$>SviH=$VM;O*Xw#0Gnm!+#cX`Ssu_s$;3dKVfo1!@60Dn%nVET{hlSxOLY7bw z3kih|OXeoZjvX)kH+Hb@)cF?wecWPX4?gL8gCj;?2OC)T>zm5)Pf4iK8+eUGZ)2Ry ziL0_Z{H{Mw%(^HP@!WM3Qa-&q@}Gw#mh{J@yeCIKs?wFe*6jwEh$}2Z?HY5jxvLh- z2fjf@S&e=BP;`~crkhx%3ST>)!Rwsnbrm5;%_}aWcQdv;8O9F9OxiOggta4|75;hM z|1Lw5LLKS2xjYH-7+aQi+(BmA=jH7?cL-LA)xS-vZq+ud5*vm3t6uM?cXKoEl^!_k z(4ELe9jn#2xEZ+b4pqE%s_`Co1<5~PcwPX4LFUL|Q2M*9gYAZe{1391B z94|{N+y0~1{g8OqG#l)1C4!Vt;5}^~?V9n8mlgfdp>>k{tVF!X_xrA8k#EQ-kJyy4 ztKT7r+HicrK$G+`BBmTcTu)t&IoV4w3I8LJm6cV=r0?{RoYE9bN=xHlBC@B|Tw`k# zmDEHc@U`zp3MQ-vX2Z#qD+N(5X3xWi4|jda z-KP82jnl`&8ly!I8{*wTu};Cjj~?`YC7ZSYPzaNUYA^9%dMPOW2-GQ;T*_aT&a#<& z_s&rCtmobhWsNLpAt9~2Zo&JJiBl_^<`x!-X!&3S04Ac4Gm>Puk9(GTEq>pz`wF-S zaLL5R#=73>aj#a%>=<9dJ;$pNSc6G%_#b%bS(AjWM&GA%!XQn6qLTLYfjCLQV0$a` zA}*+1`c?1Ru3pT>c1;B72vli3o-Y=@zg$5ZkND2Qp0D$M0k2lL`SD;e9m5THxQ)&I zR!1QXDoUrSswxydz|ih~m}HOBD=_B+S`BL%v|mSqgX@PE*8SDIxMFGO0ti-w5AWaq zMl;yei9);4L9^$K?x(wHQi6F#b#bn*n|hVqrwbBzlu37jlQ>Gt%gMP;7nmPJGDU~# zLh0kjVVv26^QhlYYlCAzFG^UG+}k8K8BZt?syNJWgGY@ry&-hq^;N=EF`KbhKSq4< z^Nl>!=seX3HwGpO3xTVom4oxvO)2rmO-J=;fw1%t8zllrtt$j%77ZP4Q9+M+-!QHKvAFDJKH8BI^u z`fSATK&mlW78Mm0C)a1F0#RfgM`#FsD=4SIC+BE>iZN}HaVe)7r)FjCV*KSTe)U(N zNJ4U$}PqeCSMbUfZ*;8C(moIh`jgcwNJX)|P$o zBwyUC-@SX6o}LZ}18D8*s|!*R653^|tmKx}ga9_Qui%E-+u9z6Kb06`Zq3!h(f6d3 zW-9kZ?{p2^-1vv9uP)eea{DR>t?J2sagc#ni$K|<>ej6(>7Pv&X}N3ID^x&XeA&c= zR+=|bR9Iz}@FLOr!Zo;8Of0&^r6E%Pl=Z$lbcoM^6n8|(p!d?X+a|gfglX1^4zS*g zh=|C~7k$sY+?pXjBifSU5U9FVAfEajT80HAMW2m-IpoQCYJv|!`(IGby{xLpM~y!o z#pAgFmvyG8*!@4rcUY#pB;{ZG;4E1O97d9V&_qMvMMx zuJpRXv6L3LBmlrc0Fvi=7}q1-+W$kx4!G#2IZlIv#-%%YNGnuNA{7sC3+hN3GLnG0 zA2Ytdc(_H4c%7^`uE%70u58kcQ0EHi;rdob%43&hSUt13dv>ECsLjD*J5 zx=>VyQZ?iDZJ)mXPJ&kNbXC|2)Ohw!_~W1Lzm(q)_hV#wue?QBn}xmsIblP;LFRzV z0@y}QLTq9#=$>P;ubI^`u9caRtVQwP?w%N8ub>*y0}T+t6WP=J6Oa%c;Ris9sHj=g zHd>lAFNn%>%!?J$O=A4DEVKp>a*D8C7XB19#~lW*+Tf$YLNNP)*6$Qlb<9Z_pd?eps+Dzb~Fj>4e|UWzar67ZG# zdspi*>%tzmY(pQjq+MP8z}d`b%3hwOvq^p~w@u^On>`XkAP{zc55L)ksDi*>pcvwv zmoKxa{smJgkaX~@suPi1P7_-_6X#t?x+{y!#4T-YG2kO%irZ*I;m;*Dpq-Ym zoPi2z!?po+ohUPv4Nt=_&^_B-IXXGzb1Rz)+zHL2f~O13#2dKO2BjmfQB;^!^k1m9 z=?$(5^FL^wJCYoeXIEj51p;aUtDUCjUhJ!TDJcq27ySFXjh|$7<5XIT$v>SuA$&+u z6q9cvr>-dtx^#D<*7*RqwU<;x!yD%5GUt8B{%$zy|=l(?*&OZyo zS!rp}nzzW-yVky8aW9Rxi;0O@>?Nxzu#^CZ=v7>8bI|;pCfv?^?5vxSM`y1-Ji2#leFwzN6Ag9=)v0tDsumaqn)L`AB=F z5+jiXwLGG;7PRT4rO%T^-ZRe>HRB^9x82^Y#IU~zPwjG5 zu=d=j_%OK~P-n*TbI`1FQ?21-^9!dd&FM!C4Kb~&1Nyp=2^=%ab%x{AeNK4(Rn~W< z&fL$)_}KLL%m5YJU1_&2^$>HhIX~orEC==4p2vIBcXAS#8{Tuzz$+Wu4s`4SS`Plh zKDuRArzP)VYR$cSoZL|+i8Ac<$wlEr7COoFU;xKaCq+lR$*N|{YBgRj^U4oZTCa^Jpvw^`nHwzed9Ngn@qO5akqJ}doa0O}On zh@j?tacdA9=w2g4x#uhst8L0Yxey|#&|usG&9tAD`kLB90HC>5(xovss2%5DXxR4t z=!8T7F@mRS(WLNd zEwt|>b zI4DBrf{eG`mzU22s`h(5N_8Tw{fOV16fa|Ma`Vj#H&2PwvRtG8yu~3z=IN^T!;QcX z8dE=%+gT{s)L)x7$m<^I=$~B!07XIl;ca5&)2Uwo+Z#vxUO#>M6eB9znwm%PkvgY% zWE~^kIXAI{EQnxGRod^MU*k9bV z(iz9qjwx_|LK(BYHrvkNBUX7imsqZ?iySPc8vk)Xp5V~#D_tU}BvKEYd}S-;OF1Tl zrXN1)JLqiurn^SXJtA0z#umnNgWn2mwbE4EWTvFRlgma)CbOO~XNcy75KaLu6{J28 zv6>*jdLD{m*q=BR_U;9n!bMB2aH65$Q7yO6M2y0 z5NL2zR#Q~e<2h#2uoYaC-58zeH^Fa)Cw=rvyIkwrhNC}BqAMlXPEeF0eGA2%^<-g1L2|~8AaTid5Z!HU7{MsFVZC2GEB6b0qor18 zH6nS_O!l3ILm%u^NlD4*&$HLFI{Q($fjPw)^uXqF>`}@@R$YD#DmE?Ve5GPvS_f0+ zfc0!&yxAyPuH^<&1~iYf6jW_BFl>MI0OH@#*~vjhe8?S*bfH%=c>~tj_^v8~WYOY% z^HAjE8{3;ZJA(qr4Z}GsB?v&@3RHHLdWOs@qn5y^LCm0a4E zzah{h=K10}XnizO@{_tuL>qtqK})arr;i;sDrbT|>-AICS1cGKN=qeMO4MZMc$%Z8*3XWMOzhWPsi%$5JgYX-V!odF$%T}P@-VC2&d=NAd+j$= zD*I<6&FO2B7q{KmT{8tosUo$PKq!TsY;F1!9Z*+~e?H;wX}y>tQn_>we0!_}$=cj* z;;T64@k(>K?(}Y+(&8pRD3EE2gz@uo=?#HVQ&bN`s5*KG@+9YZymo2PmFp<23aC+_sTEc6{Z*uK*jHTy_N_yMl+$-BfiIuI>Mvr;3h8;A2V zHw(sx?*{Weix<;$Dcyea;iQKv&*ycWJn-?cadB8RmA-_c204=mzNTTGYy5-5F50d_ zK-I7G!=!f3{w9vtBD1scEP>v31}Zkc?sCXXsNTJH_loX{(63S4^!OEESB9%Eq+yry zSqmp4Q2~fkJ-$dBhqUKhoV0e!ZaXm7u1CjsDnAtcxW2RDZS$b_?_kJ7dG&ACL_#E0 z9~BB8`@ORH(M3LBeDu2+iAGrGM^pP${k-I9(G3fI>Xo0VRrL$&L7}(_vVUePshf+fJS+|tIOkqs(c;e%ZN#0Lns{wT}ee-^*!2sY9LadVc z8!31G;9(&ET2c=2b#!x^0SR=-?Q=dvPH!id(V65|Nk5@v;-KU>qtEL%kFL}uJ_zA9 z!EcU;iB0tuTTGn0t$Ykc*uLWr<5T087=Q;=^oxm#R#r;#clzfi8dxOr7>0zt*JgV3 z$)#D83agO66}|bVhw40+e~g?FB3+049@S^LDYI#2i0OW}t;|iXl8VxZ^+x|jC(Xhh zf~Ep+-IJ=8ltB<@PWFCG!oM_aZFvtFaTd`d^4s5wu9YeiyHz-hxg>rOM3b{F)l<}6qy%0C>Fm9i%4Et zDB!0fajt7@Y;3qpbdu!_$GfmLje$2b%v+A}Li2I$0#Z_=(nnQR)lxr9;@(`|3{+@R zV5A)B-lyHt8s#8Ug9Kbl8laUi2rY6)0~jO9Wk7nELL|OL*vSNVy5!xO8s)rhN*)ph zI{kz6`d?0X%7!Y>23UEM3H;K{A99vx{ z57&As)X&Etii*4{!4ynq`Pw?CJs<*)EK%%e9e&5*@y{ z==1BN?Q(ScZt6*q_zg<3SFF^L?c-3lPk}kXB99WIaDtctBNL`3ClCEe7(D_W!zw)^3QcPc5K?=U%X{kQK%tR8%uSuTltNUY(4!U3hN`kBFSI#+$ZTZC4b0o zPA+5dHZECmaW8f6buK4=l@OxzJ9jRRwNTym)eg(XNtX~*IW;dP(lK?tchls6`b+D- zFD(adB(}p7z6}wF*(Ii(ZSXoue=1mU*${37c(;2u8L|kyF;3o^E9(4-b`|T}e?4FD zNDyr}kiPF!%c6JFMK2(i4nVNtYU^xeWo5gb)6F@**O)ZJoq!*Qiwss1c}^Ad42)#B z`rq8x#lGXGGJY}0$HD@l6FMSizNNUL{zo09o}}g+t0zsg+cKVN3KIQ2seG3_ym597 zdOAE$g5e9co2Et`jsQSuHf4W6N+H7RAFM2uolu3zwEaWWi_)KS5b_8#>YXHxBBjpS zCoL_ls`?=EAfMz3an@f0RpZ#_qn(Wj@W)X{YJ=02WS0ogjjk3ws2Ol_uA(T`@HWIU z@#_&4R25AN>pZmN&Kp2+J0RW7rKV$I>MbRtq9V^UD#)I?DM%x0aQ>0$$O=hxGn0To zCujKE!sgL?a_6YWsO^-`qAp1Yn$(MliLk3MxOhi4s{4D6nfQ?-0yiR0DZgl4CV(@x zFQ1@zf9X|h`3guPV%$z*1O)5HtA7+1*`^;)UJpUZky~Kgv7H`T5>1>)kk%ayi{NuE zcbLVqBM%qx%iR|TF<9$0&1c6gzURzGdlS{xH>cn6mU0p74?k-1=65K^6)%JPVOE!Vi^&mQt8xA-^TeXz49}FZ`48I!+Ol?GBi_N-yWM6 z(~|3Wc$Bs}hW^cOxR<7v5<$@c1xHEW%_U*~T|%}Rz~N$I;Ex` zd{IJd*f;1g5fQi;wC(wehknGj8P86S(Tq3zCL7tDIYTqW=1`85v@eA8JWFJPz zvQv^JC5Qm_b_NBn#?SCc=ccRl#D}*N*%oTY#gg1fh^T1CL`Bh;13U2_@?ybt_dgO; zDJ*99L?AG@?n(Q$0XPD<1OyVG-5_Jgj*25_WxUbD?)wiW ziT6yuqADjUcUy+LT@@IQM{?X;CfYG#e=cun+xT9NJHe9T2U=*9HV!WanR>>8%B)mj zlV_SazXi{$WX8=vGK!@EEvhUx%B?Ld} z{gw?*{^YU^suRHzM)dlne{a^*dY>R`h#wCQL=C`5s~or?^SF_f^nwEL>TmIw=!^g% z1@6~Fw{!(*`icFK@afecV`mw2DY0L-B5JKe;5 zHVdk?Wwq{Ks@?xiJv+idTxZ8{I<`Z*39h5Zqt0u98g66 zEYo4ZSLG=3rKAux%1g8Nn{+t!L@W8&#)z6u)eR7%HD4wU<(^sOwajj87gAzP7X7E| zUbRY3r5}=U+}>dF6uXNM)!BDa?D&N}m+#-Md?v)tNO!;{lTEycoop1sauj%bIRxs$ zW@cv6h!1JDoFj*Sz|4&3665TLc|QgS7C^Cql%bO5ysT{bb|yQAc6YBdO(%&>xb>-| zSlF}jKFj`Rz};n>U7>mR4-l#sUp?~gMKTHDP)iY~N_ zbW~PINN7K1smtOo#1+KxS}?i+)4BL@f;jj4?ez4+esk9TAzio4#96IM)}Ua>otgo* z)5>YxaxOkDmx9wswYGk#jpvHW5XjJriVw_-TV#}a{aIL8F2Rcd0S_+z_q5A@2S}gT zcMEq;?deu+@m03uOwy$#Bf*w{7VQgSZ7J4HtTMc|EhmHw0F*T~G}P?lcz&`vshd;;5xI)&03X9&6+M#+B6Ye+9^ruy3aj5=vk=V+?4(4_ZqFj@Z#@&Q@-G-xi`Z)2?rn3~V4pZ8-qLE1ep&X- zHB6H8HV0(*&4qs5te(u_gzE$%F~QjIcX8OvvQNsSK_&sk6z(qSz9xGom!;qr!n&3iXUlsL6*air(S7d7O)ghm+h3~uEOvzOFY#WM*I?#8J~%>)h?q^ z#LG>a$oo3OC+qAd{OJ0k`=NZ>7}BT;8jceseN9&;c>D%tSlI%rJEwF`#thDbAqo?O zR^JwA2Bf?2(_o_;Qb3Fjl82OnUedOPQKyN3b>QS-e9bl=67zwULyxEy?J)ckBZn-V z;g&jOEl52I&GaiQFf1sh521P{tpd;VPROp1SlR??WT6({{(Fxh{-&=u>nr%pL7%<| z{u|)09g~EP1F;6@i~K@QhKJnMwsp)7y)f_d(wD$%{&e%0^2@kKlXy9dX@6x1vIaJw zLyHw1|=O9MjXM)9#I2M zr=(g0l*6ud`SsB4#Te5zYt%x$#Vi*Ymwa7o5;AVInDOgU9-!23xf?IL@dZ+_Z}@kB z4S|J+@WR5c9xMaT1qI;mzLGSUI0xsUXLePf4g=Ju<>@%p zYa-<}Ub7)4!uillr)43G-83LTwKRu3}TiYaFFRDCxa2R*j&iHhR^jQIzGUF5Ku-w zqpnJT4O0%LE92%vi+8HjvvQg_Mh9euTO4Er1EMGBuGI#n@n+>t-jIIi8~jE6ag3AfA`e)C4fdOC{H7k4j*vD`i4DY@34&x^nUGeL@9^#Z?h$cL_^Ys2Ij1ovGoJRJkWM<7k5Cj zrl!J>DjsCi#FEY4{9`My9`6^_5XSm#w>!w>7ZjYijB&cid4)qO^fmX-`wC|||4f*X z3ayEbn?h8KH>rMYS(Ij7AhfF&)g53xg9SpUa+n~(X9}w-p2vLP#X)bi`T$NuNNp%> zM?K*~d-}B&90UXsVTI#9FE#(5P4;_zhLNn=CgJeBbpXVd_#-6L=&ArUKiZIIbs@~O zQG0UFQTvYah5i=u%F_|gBU`>K!?^=0UT~j5L_JzV+~go1l|!%4bKP7#6Itm)SI169 ze6f!6ZA5DcCd?&mjw$*z2?*_88Q_m+-tl_y2^FNT$`A@1AQxchkZ<9vz4)BJ6*qKSM&=Et|UI6b(>noL-jjShYGfsfE@PX&0#1%>+5uZpR=c+`yF(TOX;i!{<-Tib{7=7T zfEB(nYb8nCa+o{81?vpX2e1(xae2|Breu;6} zpA{y^^`jDa;d!bp1}9oe{e0D8*-mU#j0eQ7hAmtl`;@?B<5-~mrOSR#`>#C3LiI{Q z3+jG?4(M};Z<3`%wY4N_owCbE64jOso$TG2c3;1J|K2e^lo3J*HtNX1c{>OxHx4;0 zuw>nOoGtiz>CC$1_lQd}oH!Wo+`f$u4e3mBa`IUP?*Am%wt}msi}bSEMvQ)O=j2;T z;3j{GAadBN{9emy9~VxxMJ5qGRJ2CP@It?PHC>VVih_$}y!SI4ppI3*_N#3$(8yI> zaGYH^fpp@(-<6k{Ssg|VLEAPmK&cNZ{r#s;XP$d-efz!Gh*rk$S1&y8^Pv>{4W|5X z1SgXm9Fx-#QFg{%BC0RdYrOamsSPOE@JGv-tbfn!4NNePA;%qb^Kh78Lw3@;Y|iB@ z%8S8(GHbUQxXY;i3RqX$G3h1xWF~|;(E~p#vFRhPZAD@rs zlGkJ#FL+MoSz{y4`gwK@MJx^k>;9yfZ?OFm)FAJ=+BD+Q#9|aUDal~8!!L$l>bS4H zg`KpzdL5QgQJ>juS;r%Rd{LMB#ut!UargfveNb=X1Ivqv71q}B4uuzyG^$577GC?n zX?ILpRr33|fgXR&(}CMtf5G(nAv6~k@jQl{>Ea}|Ved>QId;W?{de3y*v%nR-&ij& zpTQ?`B`X5xNJ;<^F!2@gm#RfU+Ti!(P~G0iS^S{<^tjPpf7RMxYAbsrx5BC7=ckw< z9NyT4D;TJQ9a5iTNUQD^S4DbNQ+4KF&J%21UADM^u&+4gpJpBs1=Q%2R?x=`MM=G}{-@XJ?4o&Wg%(;NiLEkI0l zkedM9;b#}*3ng#s7ixVD~B>fXDNpy-9aBY(!>vBY8J%T zBTG5;II%U&TR4B`tw1{7BPHbrDg}Z9h zzg<{ITmoo$%IQ0S^qpHjtlG|B?28fvX=2D@b+ zaPk1^H(6|{V)V-6kabCAJs6EY?96DPU}0N8;8 zWmX411)m@FXS;gF&h7``P3*JvUW?+ghL2~{7*g~v9*;62eh z_5&p^VV;|Gr{aWAi;Dx(E-zG`T-1?phk2o`h0=y-Luv{(e z?Sxb?$_q8Zf>Y||?lO=VW!fIK3Nri;uOp!(DD}c9Ok7roVWPN zaGkMfMUB%*D(%Cl7YN#353PUsy!?}Edq;(uQ^@uX$CVz*+SBW*P(%c3=sPkCIio_K z37B@H6qZ{2D>(LqFrp1D@$pQR$f#?Xrsrii+v2%aK>ZxrX@o3NR9$_@y_)*0R=dRgp)^BXm6DlRho%9rlc5m{AC(pNbEPm#qr(WcZn)v( z-nIsB#*=Z8Ybwd0KE}0Y53e^x*ndRn@zJ{vU1zu;!<*q?LJB6{S}noDU~yh z<0_>e9UJGhDfMG%fJr##f zS3=OknhW`dHOyf4WIy^p;_^Jx!e(2Q!k*Yuf)KQcj}Y|xa7d8qU5n;yWz3aZ<_oBu z6aM(xxZoFH2#lqCh2@2&thd(^c|=mGCo(s$H+U!_BDe(i8)|BTWe_pYuK|WZ!LK~~ z_Fe}&X&IF;N!exlfNfDprKcCFzlQQ|`^Yuv#gbC|l)JeikZgj^Wd!`Z{DhgYBbik;*e*dUb<$a9sB!o6E`6J2l z6%dsHKk*+pu?f|l;fpG-oN7}lSCUS5kR=dFKW)rI27;>42a)0ny{Cdklp3U4MGNtf zA-I6<@Pa7y$jT-jP#?Z9atP<)hrKk%1MlQYyuQ_5y=+XXcB-~HrCdVM+>4$1@uNuv zWLg2>?tbm+Gne6a9OHingtT?o`iOS_ZZn@OP6vUy-Ihr;brbTV5cG& zv#rxjxv>ivMb9Wn)}`8pAc)4H*6lq;N%-#_8i*bXWw%7@^1OfJ)vDq*SL4>R@1wbP z*K4?|!Kn3+Ok5xl?%*(-LhRlgIGV<%L}z6@QwIhHV3s_=81enP?Tf*L8z2+lu>ps* zXPEo2)qs9)gqwmw5YyU-&|J$!LCmdixeUw|>pK9pLzLbS_OATNunXbPF=jLdJ;pZ1m<0Rj2fp9mi=M^hEf?U)R8sEMl&m~fV z{hj{%$z{ZDl4O2tK+K^4<{*%hobl)y;IMBw7YXDUvU8xeW?M$g{7Io~P!Ho2w6fLL zCwI>W!XYJf>nH$C$4Mi7?!{}quPdwoq23KOt#8+`d z;8_ylL+g-G`Q7s>^t+gmqn%^@(x;$jM)x9V#x1Kd8|0ddb0i;U%NgJ$etb)umHN)R zh_9{Pe1SJ8y60SpipkV8VIf63j?tZ_cP2!cjS$j^PHpSY{A<^4;i@IAmUB@w+(gGS zl#?T`hY-tDc`&Ne|(fN;pn7uZA{LhzEFLD4zHQc?p1*Zq@gVjV*4+uh+BRGZ z3%I%et9$h;BzjYX- zc6RVmSAP%j$dXIH7aj`?{j~{yNHZDn#;v!?h6SHd5fes#5sR)ZELGXdaJH=M(NOY` zEEn|H_tnpT0%Leb(y*GxKqbP59q_9ZCih&fT+)4w0|dF6Jy@U{rIqt*U~12Hi!OL66MUXWIJ#7O zaP0x^)+q_Ag!Poy*>^k3FqGXpSQdg*9JhqhLO(`mT5rcqhSOmbdVwK?W!!69v)Qlr+_vGRXlDNEXsEFPYPpazEb`^>+8&O zLaF>RX#n7swE*=Zlj-=^@aE*09UY&&iu1ATw9?YMy<^KddH;pT{^KA%Yh*-q=>kcp zCRktSct)ZC5Ka&GzBrJ(4{{M0##5c@2YiImq`E~;`5Sv7YtA%7Ih0I|{tCV~(&^%g zj^0VsYb8#R<8+!9{sZ~vo8hSyWVb`1?8Uv0nNJP@a)1E{XDw;ffjP2mumU5LjXXNE zw1Z`q647)#pnrr^qBI4~&YbzwPo^8Q@t~p3n2Wd6EV125h7*a0fN8lm=5V!h@bJWG zMlS0j%`aXy)%e_tK^{00PEJitt!N-Q1}bvuv&dVkbX=sh2#xxW67~V;(ewPslf=Evb^5@l!%SZ?DzgB zddE`NnXYMZ-!I_$#AyuR=HtgX1RVlfgMcaeT}G`h-M`lJ#k)ze`deXM^-CnhEbiR8 zUzktBf{i*6_3i|;g~5o6>UD=zR*91~sSI#p@SEM@!iQTxO1h+`m;4t6i&lVjM>~EY zjO{=m;LUecQmX+$fYB#?Hen`uccXGN+J!XG6c#de(3c0r#k0RN-aWYW2I)EN<6O#K z4)|FoM@K+Y*Y?9^$(fffm~vKB#N?V3OhuLwZbkX+<3>l1>OcoQTn~{iax@5w^aYR{ zDD39w%BU+ji+EAE!Z89N4?FO+kZo>Yk=x7eF6y$=SAzrUK<{-0?QyjoD}> ztR-#A?h5p;9^Wp{4x@l{WWrLM74YXIVAx|{m@RHWDS#V{XIl}rc(rkDiiR*j1d(bn zMj(h(zAfL}4Qp_rzv&hL|3gsg&G_OL#O(^KTs|pr>KASlx60h zOZgk3Tp;@f7xg zV)@~V34X8n9_Lv0M^Z(V0fN-eV;UdFv&J()p>JVsj=!6jK#HktmDtvxNrx~=EhLcG zVZM~8ni7KwBge)cMsKcg5{HewhQG7RdC)WRsq`+kW5an~zdvS%P^r_Wc?1OP3)ef_ zMZ*MH+1bOKw6D8R)o?%0nm~cig5Xc1l9pXCtf!|7kZq#CA&8(2>K6CP;mW$-+I?g6VPw9(I z*dP7wVf25zVkpWCX#%nUaN`0p`0SyGIhVp;TFj7qn1#V1mFaL{vZ&Y&Mm|pnX(gtP0&eVQ?(c zC#Ofno&XHYF=Nb%IeGGtJN1sH9%oGO*^KU2O`Lgb*lhhE0}>NAY@zv)x<8RIWFrci6w1S(I3uhDR!pHR6_2FB~(-nGG_XTegE|*{By|s zs2g%pAZA4SvjL{)BIgwHLxo+eL%{1c&ppx~JUANbQqd2KbFeHCyZ2^}?DY^_o(K%{ z_LeQ~m1#q=74)w_;sq(gKy$pbh4ya7_shw{IrQSjqo>QPShe$%G*}`--cT_L7RTyI zrRms1IYU8rkW=Xuz63OL%OBZ4ulu|nLT@DTM9eg!(x$9;*CgV+9a)z5-CN%OcxMEn zw1whYVV`NFLa4|Y$mv1LvM@74sL>P-;~Xv|be9jbQ($|}%DN%6K*5xzBN#gcfVK1M|@sFlmVHAT}4F&g*9cOOIdNTQ2G9E-azq* zefGP113Ja&?H~h_$RTOzL;+=t(Fu8`79nEmG%hSVPutY@i z@Him5_4rbH3IZ$rJgo;xS5g*5rjzdMeFCW0?zL4(!A+@p5;ucqL|tcEnj$5G-5LVm z!wqkgx$+C@)NN!}Ar7rKJEpY`akdaz=HPUsJ5X}gDc$4`CMq7zH033#r+K!=Y!&wq zT#td!ByvlBtx~U}ay% zl_f@XTv_MREvu4**jTioU8On9QbBoUyW&2=zfUM8M5^A&6X2Q}Y5(%V1^U8nSF%0a ziB5HPH^3C~s`|x~$Dy5`o142({_;`PC3c+s@*OHf4;9>)oxt9oiqsrr}+^>I`z{#M{Tn$inNl*H<&&fn8jnFJ;Xz*)Ei_W+`lYTEJZqwxU zsQn02JeLzXWbkJ~0{5atZxWIAYAQ*qs#mZlH(VI`XxKEq7u1d#C-3t{hiQZsVnFQR zJbROI6c!l#VTQ88FEOSW!GS(LK5&zPUtzj0v42!wxyj^CJfppQ;WP0y=BJCg{_J-@ zwyGl50Bw4vAY~CP%u;xCSCF5De+hjBGnrPIRi1{em6amU52S>YR4te@rIBWnMhvDJ zHf75=oWSVnUjU#_SG)h0DRA7xL{~q`B?#9L=F;MR8rvuKCe(Y=0=HK_ zqCOC)nFZhLohD-^`-nKc9p|I}_B_dGN%DOOTQ0leA4o^le5V;BGOcU4!{bSfYUSFz z78;ANcgw6E+`an+l_NgYa|F$zT|>8syQWGi;c(-zxFPdtns5_RfA1qBvMnn+$$=9K zM)}@L()*8EbjqkdFf2iE1}Z_g1PC+md?g%q&&hqo@+F`p83^2t;N)^A?xbJQf5i?6y zS~KzIFPvo{D*qA@ocIF&ledIQUgNFEohWJ|AGEw*Uu5AF1jaJNQM z40NA0xp&<)hxo<>1J;g@&wV8C5Z2<()k(XgI$X>CviQ`MK!WabkIa{o~0t zza3J_^Iwdf=@|vtw0?r#TUu5Y@LPBJ?ZJjVH#!gD;CTyyF|&}x5H?TbKB5}an^T_muZWA;Sc=MWT>Se0 z@xY%vUS0jo-Owas^cmR3#HUf82hMc?=P}_?O^6n0YPQ@U|PaO~$@I+EJ$c zMc~e!Q?VD~stS$H-AUdv=VALKGW&4zZEZLw@ps_pW^Q32S$_TVmxx|R01}{;XOyBU zv3K~syH=-s3|+Lee{EB)zk7jDLf>*y)VK!Mqs8!PuE6GMC~V+#sE>~yS_{*uy>%T= z7Ld)Uq8wX^C^tld3UFU-o!q{oJx9cLKqwE(+8@$RU$2FH4bh$odJv+<6XM}|(m z3gDsLyv=c|tNqp^vVdbAyxS`U8%fO@KcAN+-Q8+&ImbsK^1y~Uz5=0^GOs1L$&THI z1@Z}GA`f0(R(v9})rqdD;$JZ<^CaPdBo1NZ$U*{O&~N^nMQrvdemZC!H~D+iE9w*t^?*HTH{jAUQVp%XB${2 z|B?$|eWJVL&^fD>9snMFm-bEkA2RVHZY( zDv{xvNbSX}_X@Kc)Dw*3JA)A$`4=Z5(6yt8wTea!yBq$bp%9%E^=D=eEs@>UQ-@6*o2`m$NX)c|eueJob&W*UU;qWP zDAlR0(s96aQNH*K93nwi962J%WX+`;lv2T$+lSZjzxdOPj)c}tM*9@`33WNiK=m%d zWPI;?U+&ttm8C@9V&s?|E9k}iR6d!n%3U@(E@A8p2poQ9E76cl%x}o83Vn(+vdp?v zBGqUPkPeu|Un`*?xjn{11La7Zdk%@n9LVnza4-I9x0mb45=vpRT!`9aEJ|oz?3*rG zPA4UEB$VE|z=0XhEM9m;c8e6`f$~!-m8InPwQJLKcY^qX1x~6(T|KvL+CMRoH zGLLNpEpq6anL7U3I`C+~hX_zObxL{iaRJa0NE4w;gzqcrDkWX&@^QJUHujh|WtUWL zST#{kTDNf)|D=55bi-tiWO5?2Pmagxp$%PgP7k3y!fU?~QwKzP_;H15|eb0$-ZElIGpZ<_6R0;}MQIn5CZL6w z2OhJ4biPE6Et7Wzri3Ou$lFWk>lBYxZ^UeDQNK0d-A?~hq~Llo?X01X?1OjTT4Pyy z@WQ@$A^zb=&YN-{(=+JFtj4rLJOmqp9*p%;d1HuWb-%8 z$ENcDgu)1CcCES`y;pPP6IW5~U{g)R&1BAU{UTkvZf^ymy%b+MsL%n7D*ZjQjPm@N zwkD}I!|yLz}}gl>J$Kq`}IUK5{@ z7=x;zcos4WMZ68qy54r!XKx=06MR5V#gi+6_7+^J?j8~>v{6>&Mugo+lk4dJ)zxCf*L%0bgD^DL)t$z@ip${W zo4cPdz40zOYz<5hEf(rfOE^^Pg9cmG{B6WcerM7qOfF=?&o+u z5%p*z&{!{gLKu20ER{&iU=3~p*wwq2OLk}G>oKltJ5lcMy2_QK_snV7*z3BghdD~uy}Uo$ zgg>We8#i~HKPLy`l1fjdxE>FmD1S8vajI>#UVOX+;&V@Z8TYMe6!az1m&H`cmUhCAZVeyjz!2kZQw zAN_O!&oOga#99tn1QPX7^~k|1^cSfjnm{~)c@IG#F%Xf3Gi({LaeoJ4GtSTF4|-8z ziO+c}Cnu4_4|7$a+NRAv;cP-Y9LH2!2fjw%q3bL+M{i5+-0Af`|Jd_~7(0dgVsjID zyxb+t#WJA+$Lzf|MsLkj>LL-DdE@4tNNXeWt#jLyH$6THY7@m>=ky&<^+5(>Z~qtp z>uFlQ9sgU%mSQ}DLv0Y2_!Tbb&d1Mmg&@F|S7`e2XB3^fU>^(*CdkLeb7xljsFn9~ z+KSMm3`-k7;F7eIdofu$>2YdrX2@^LtBt8AO1f0+_OV+3h+FEo$5AGhVx$OB4$dPa z){8DuzCyP&A)JCtb@#<X^0^lvOoZ>*~GpR=^*g$0s$4yfzFSoQErxqGEQd ziE$(#pKazv($}UK?1Sp3W`{945Gw&f**jiiQu~7jNV$%9!@G!@T=3X2G1001LUC-V z0MJ#J)xH7{s7SK{C4@1&wqe;LAv>WnueM_>-hG+6*OpvlCz3&Aj$m&;if4iM4C1$02 zJ>ASBSw5*fGa~~zY#252;Az2Hk<=dP$UFUPZYgt?YNlWF8_&VRzEOI5*SJ%EuY`s* zHljNTa_s--0^?^E+U4It^3l<+KfvRp8*q<+!tt9vaqTmYh0jYIwH<$u55e;%8n+C= zC*z~X)Sh8;IodepzTX>yl3_nr58en{Py8Ui|M5!uvj|YbTdu6}v2g1@^wS+f=JY0t zuLjV5;4m}4&FHPmW|?9285vEnrfCr2mu`*G55y;1dwf*jJl$}LX$&PeSk4hhq@iL- z(QWsvvER3kFQo%8aCv#T>hDwa_Ig{+u+O-nfH!WlV8>pA>zltlMcv%aXMY16i}GUZ zB^5YJxlIyGP)=yI!>M5qnKz4MO{-RD|$0V&_tumU%<9ufD2Y6NWs32A+K zs%bhu(LtambrA{15UAga@8QB^^B3%UGh^F_B=QCPS)wZ@0NNpSZpYhgM|JZ}TN`-1 z#x^5*49}`uhspWag2Lu4MNZkd2d?V=%L>YDeq|sLe=6}=GbS;@l-V{N9G`xp9pS*V z%p{6RO7cS@tKduB~}$@FSsPvB!Zn{8WShJC8CBg*~n|} zUik6#(-V3t4i?DAx@f+FKp+?ln0mH%u!v`j5S=(hak3N7BSdyQ zCMqnBAj!#|Mf}1lo+uM@bE-TFRvr<@+->0ly`>hS(K}WL+csUhl(xHcyd<&z?RZw+ znu=00O^8vhb99yNp-X1E^gkyjCT=hrUE#rTb^e zMxE8IbYjY+gT$GcaVfGpW{ZBJRO#-n7H?H3Hh3DPHP@p~nCd&;kPQl)>6F^;}DMJ zNOJ9EPmpmb7^%D-44cC%v{|@4T|??vRb^$whq0*BT&y=tH)wt*GK+dKJ7iBMG}SrY zeu+^4fqKEjP>`#9Zf&6xeX3_QJ zkV!Q1Iz4IzRq`?x7tcNc8078lDoO3i`jvG(J0nBhyDJ?>-9%K*eRE2N^q%t1bU#i0 z?|mNdDUDQY)I0AfHgH8ns9}QB_Jg4~$Je{am`9*WIWmXCR4P|mw|XTbk!lt<_-ua> zc5b01v?9#wf6?@E%&he_tAwbM_o?VX(f=^B}VBnFAYBHrE=wwB0vUSvU^B(V(Xa>AHCH7~d``I&B zb2JDl0aTsHYP*z)sc8sMR}gwCNu(^Qrhq39cOr$t-J>U*u>%(@~wqX{Z^fa{MFV!7UMqm|?G3fWQqD@Rm}WafhF~PRgB+KVd4U?3qXJ z=A?E&v%vWLD|5TX)BD;UuuZ4edV5SsN_Fhn{9XAv71m>rg6XLRF-Mh&L~{(W$^Ud9 zbjFpz|FAk)-e#AAq9VadT{Oc5vm3#k`GPeccTTj7K?b(C5{j8r)GQbH>)G>IDZsO& zblFav`sLeFVDkVGW;q`$?}* za0|f6pXDv8^mR6?2);7~alr(;4`iO~bL1p6`aBABwa z-!Mq3M|RTIF^CC?7a9I0q-Bt{MHEV?5`Vq&f6ywa=fe1%uJ?=Hqa$EL;eZ=NQwKPA zJV6ngpm=tX*@qdQNFT-Qid-Q`&&AZ&gYK|MF3mB8E095Vr-aY%KXz%t! zf;O`umVGpc;>juUWQ>#KjnE~60f{`=vs_Gb_0O+qJ#uRdGilB6@cX*+Bzwf?alL3n zlQqc|7wwa}`1$ka7f5@li_cd_9tlQ7A||e`?j@4To}}mK$<+nz8R4*1K+Ox)q!fgLBTLSwz}mSaYGdU{4iL>%k&U^`h_YW9Sip>@}31Cbyx z=>p`e+C6$+?IL1dk6=l{(C}J)!fw^b?n*|tH;^x&%zhYc&YnFBnzGqF+<-MKtM?GB zRi07FCNz;w+V{fqRGFdaGP%qSx#ZcrO&P$bP7-4vE@woY ze1|%UH!aY_L>?#F2F~6{A;L19a(;7Cy^`(&5n)u^Sck#XPs}lrl10^Qwt7}pFP0Z3 zi~8J?`@&T5anpFe#P<}nWAuwb#?o8rSVz4sj;iuKm5~CJvWSdWmAvotmJfh)FWxV zEW`97gHO>h`%iCWY&VyEQhT^hKvRvaYvo^9R(BA%I8=EikSW{G@jjG{KJ1Vo#k~7N z-Qw3$pB}gAnPpm=(buJ`6TXaHE5$PFZJUFQdD~&R^!7%WkGwx?xr+?69H^Z*i}VyU z?`k<7(PP84^!1@t`6FB|JYFV$?T*_0XTaM@1n}lt9R}B6r#WMjj)(b47=-Xatx@uw zJ$fTf=M6n5Cc05)V0Y-kz28ht+WnK25xWwa2f$T8m`-GTVCxK~9p_#NeM)GeiFzvF zjfo!vzOnto++8?+CHU>FFw0FE96$fDnTQVJ^CAVa{&_yRum*dkTj)YNe3-h4)KLlI znT0o~559!f7SA}4{Y*6{8*2(D-flIVEKru@L5KuZtci%WZ?=7oVZ2ho)e3e?lkUSl3p#^F;Ps@L3+9MguCM(ETS^6hRo4Vj-jykA9v^#vpxW0j91(Kh?;3RG6!f$C(9@1mO$4i_X zAn)*=LGOUHi>>C`N)I_-3%rqjt>)O*sC05EVzy&VV9XmzrL1myi~~4+uu$!jaDjpm ze*^y}{#;OhL?a&_HntCX(VLH&ys=EyZdsuYc5+s@ytaGbRnpaUp`sMixtPxsCDx?v z3iv4S_NMTuh9L+Ac04M--_PT7Qs@9K3Un85Wrp72 zWH@*+{&2|)tEgOh>;L5%lF&c5hjn$yb-| zSs6hi{)f!R^n9Z1-p(s?*}Tk-ML?4zGTX0=9)Pn?){z)Ai&Yl(N4632X4Q2!TFI*L zz;YW2AAV$wy4Ly(V1nArz2ys)Hy2S34IObu4hkSvLZOb=w6<=s>Eu@GBAjhNIz-nFktvbehf@=Wr(~!@^eQADliulSNWXyxkBM&A zu8B1cc5dcKA&A^b-P*4;N?D;qh1C_$3m0zt+-vB)dgGUqyMOwQ&rg15+V~HrpRz9b zy?WF)z31i#SJEfs1chSG#XuAu)7`@P7|)$H9#m8vI4~XA4M*1(uCppvzXjU4GLNw5 z<3)Q0nuy3LGRF7W$6l)c%cgq1o=uO9u*pNu1(LsLMnBDp=7yeMOdcAhavN>#b*d21!0*PS%j6%%|4>IZ5r?=ciM91na?RgZvAkEYh z2YSw-S}ez8OSxm|bp3NnGd~I--`r!Zr3}p}L_hFKb&!&UoW}}p#C>Gokkrd$pfSB@ zMu^t+qG)lSZ{_9&upE6@j5&Uc$m7RHWMO4xWooKNC!`~nqH0v}t3{o*GAGw-C5_w@ zLEbMBCbC^nzg5&DnSjVJCd$_z>pEI|CuwyNM;tU2$e4<9i_oXz+?LL1Vqwwt*ex-P zNA*FJ?w;+YFLq3L=zj1|o--`H;}>+*)_`8@zFZaxGW5i=5Gw8k6BMio7}MgP*vim~ zzV*FN#2n#Z!LY@v;^*i!`dmcDHxzUR$gof`p{#BMkA|TnRlWlqzxN@>2ikgVP@EnK zbsu0u0Ex~}lui7QNHNQ2_Ji7O7?n+8X+HA$FaBW6fsR>|rU4cNy!=rHHitW}vpIYy z3G~N=240;lvcneKx0tf>MO9ArmsFpjQ9@W5K0LclSx@3ooP?EzP=ute`Sm1UmJe9+ zxMIs9uJZ$#bnpq^X*%+GvUvbYG9G&5cEZ}U+@G^*&M!#1;Hd9%Mp$zu{^{j-{m{Q| zZF!n6rsBx&1+q1~-+yRXJX0~9{`kVz2tQ?k(`cq=(mE3mhO;L9{-zE3T_*NUEz(W+ z0wabHN?r8w_kaHO5eUa7K^hX}YGQMauLTXhjbGaDkBsd@_nhcAFL-)f65Pm%cRVxp z481e+pJz+i3Qm4ZDB%Js^EB2#6f98Q2?S9v#C$Vrtud42k-(S(O;0s zq0A1B31?}pY<|ZBx3-wQ8<~zl0>#o3>zYI&t+_ta!HgEvcf(%l*L6S`C(!AQYz-9>^H^PT zBekic)I|pXzi_vJ@xR?US(KnRAj6dM2~qesY#A-5)Af=ATzppLExM7Fi3hU4wm(Ue zUj^gT2iVTE{9eo9ZD#mmUgDSPcLi+Z$c>9wjP{HAQgVJ<)H_Zke%H!6H8A@~7%ela zK%iPs7Z?DtIO_IL4vJq?09tH-gQTdAoOjhLtElQ>Zw_}H}yAcF7xu5D2ylkG0g_s71%0Y@c&l?NMCzIs+iZVO$9ScbdY zGA<0icXcVZ7SMeB_Vs0y%nk)$nZ&SOh2UxQ@}US=wUV**TX>zGi_3fLjRI$9ih#U% zTH2h?l#k_=y?@-}n0t4`zog&Lrt6*@EDAgWQQpiF5m8hB!9SGCRq`w!wO-eY7avhX zap;ivi9{vS_!h(95$UhVBJ-cOJNCIGhSzuMJ8BDTLfX((oeS%TPKvs8pm8+KRd0DG zs!>r^9_s5Wmm|Wvudbp6GbKG&|mA!gVM$Rwo z8-IfIOiSNRFQVLD4v{E$k4b4*C=rIQa1~o2DEhw;Ubc%=$J16rBM4{DjVc8~W$o)b zi*?N?O`w;=r*-xhmd!AjExl76gI~sc@JF>EKEtMRWS=LBseWslTlzgS(CVW!5k-$z z^Ail*IM3Vb8}8^AkyQLhDF#^qAAUamyd%NA`6sUR*N5(fJaJuSl(WQ1OM5e>N!Gfc z5An9xCk`q&0iVF*?F<`JG(fadNQj2{BXF`~;YPWLquQTMt*IMO_y(b87q<&|-giO= z(A7r(5uk%0gF!=<5r8O7m0t zAq+nX-$z~%s<3Lk)WR42h*dFikUhvFve_M|J*7J(qD)0cZ9DmyAx#cr z3%(5vi2xdY)=nh5V2yYJ$slAc?v|Oe3iUj!ag&6E1$C(@YQ+>BRogij?n^)yVq$mI zV6%Ylt6y&UEagKhAdd*Y@i6CWZW9Sad}4+V!Iw??OHG&R3-z?j|C7;FRJfFnp|7wt zKAq>4VMpF1y=v;dw0O1?g}aXUWr}I`@^3|oxPfy-OkTslmv4(Jv8>xxu?Cc!i4lK8 zO&I~p_S6dR-$Pu1wo#W?3;EQF>DO@cm6n$eu^7>4(Qi7E z=d?}wI`U1w!x%swPB|Ya!^L**vi&L}6BCi54+PVLlTFK^)b0x*D7m>$7HB@#Ze~i% z*8QH|vdw5L%Kl9*%%)E8o`Bl4(4nJwi?*gmK!j(jM}1 zpjzNy5h2YiTeirctcLVZ#4NtefDKnk=r7&91S2^$FYsf{C?m#Ijy4+BI4br^3-Q1BRaE?5Q~XFhB!2ZCkMMX7 zN?B*%TgfmHQ^{k=)+&867Lwch83$+|xE@TF!EuaMV-^dUS;KQoaXj}TGY*Eo(ByA{ zlRf)%VIQOOvu_VZwIS-LzAGGQZnp+BayN3CDhdIct?#%)+$pgp?6=cXB`^ra;#q zQ%)pbic0K}zMXaGIk~d&vT^qDWceIE@?Y(OAB0+<+JlPs;-<7t=)#nFdo5Kz3>O*D zWm^Qw$tfkv14FNIgw;kfELbJ9zNbLuqmNn3`Gb$UQwvWH(Up9Y{+CeBk&%`5oJWvV43+!FFX_oCZSapok)J| zN4R4Vce0a>9u9&mA@%RR|G7p3tp^RZnM&MZ&0VKCrK-Jls9cr@c}^wa-$ubp0|gQJ zW_jg0Nx8Pr1y1y-2_qW!UPiwGc~?JYt?1um;}Lw0M1vXVWulP)myq_| zD>)Yloshjf^`|YhfZEN`8f6;6S+e>*_ zSqe#>GIa|(mD6jEpE`-p+a8^zc05 zyNZo!<2&@9Y#qY~NPsIqvT1k6c$$+NXFi|NxH>G=k5B>Zw*-5~7}GKy;NI04qG=Pq z1Ew2ZX4=M4LV-&BE^m)kU5w`YV;dQpH@6UsbtetCnMXZ;sFT_6J!@Y8yu<%^^JaI7 zcwUlhO2r;qo=vV8=20(3eoJ&wq4(*G(n8B6;Wp&>=c%7Ogll>Hu3`rocXB??Tt~g> zSV8sB{-ilFndRG$lVLB|+OG2{9nCc*K8Cr&7M)6ecU ziJs}o^-x6nM1H=XN}gm0U9lOwIKaFbDJ1i7EUBMTaz!^U_U2SR)zk^!w*)g@4+ExJ z%~ntLb1w{CNSvIU z?#nc?xB3A%;AG=itlDy_L5Y(>G~}7gaOwAZF=UZ1X!HSZOpbb1m@>N%498pwsNfPi zGs~8EvAMt9{f~;A{y4tEV6y>`74U-P;~kz;;|YprTtdVf@-KgS{B{#59-ykD+$^qb z-PzMG=g&KH|CQ_R^Q%GgsQjw;gLJvkF9?vJd<~CMe(E;)G>4@>I(*zSoKe z9&pBm@6gl7u@bqZIW`>DX%Kze#lt1$jFlRjEM zxavA52j-R{g@Cn!(m#%I26>>Y_!g0g*EZEr#&(2JW%a(-q5f{KUA)*nnTn1*r5~Sioj<&WDowvm_HLoL6`gTiZ09CaR zJ^0dT!*Yqm18#Q(*(;mfO{}e_kM?xkHLNZ)t&}Og$61;NF*Q~;Vn`kHHfOIPcs3YN z^rW`sMxuB5uF7x2-5X7Bt_$$^9yZ)rD0b`lVFWw%<}LvzGO%c#UW~ATB^`~;3jnx> z>K~c&4xHZ^WIgf1!{eLL`;YkO)y@rzM=k#bHJ2yv6+8N)a~2Kkd72>#{6{(7B5+Ng zYp4%ZnUThWq==JXcFOb~t(I>zh3#1H)(NewM0eb3}RUh_PoVpO6{ePJP}# zeK=~hZu$aEd&JCG4$MAS*eX-FOm9Nhj4*jj)SL-cu*u$cPWZg#U&?ZQ&nE>mbcq4o zMrs6-;V#2}_;Rx*qivGiO=594hXkRjN8dGat=t9F#e=+V{Hg*qf)jILD=u3wLpH`?>|1rr3`y@!x`+3e0{pd1(sHqE)( zy&1Y?y@qh7Xft@ToN*H2hJ^Yu<=a6I2Nh|ButN}Gql8G4VfcU;Xn#2V(oU z)@D*`6US96Rdl?>vkzfmv^jVVm~}gaW1CJ2a8sy2pV=asx3g0fT``}sVz)T1{Y!Ya zb>)nO@04mi{SR->ltVB-2hG@MCnF6X8vu@~d+&(4-mA6jZSLB;l0qdo%jefIEWkXq zfg=?DyrB8!xM=Cc3fmGWxwK&*4AQU^-y)HI`q4iND}_msd3Ta@{yfc(gjuEKof$ie zJd<9*&W#8iA%T+g`S9#MY+wjK!dw*l!}+eash8V!_Kmp;QHtwNrQ^GVBc>KtklPfl zAXI82?0?H%iV(B{kqA3QmrNK;SYnlZq>iK3_P#UA;3*@c4!}*vY8-V1X!o&%g<=}VN{>J zI9>7mV0-LVa91a`tiSzRFhw(1EH{7Ue+J0tsdxKFV#|h^e!CkHO z!bwe;)-8gSN@r6tLr@L;9#6sXDwP?DWVnmG#XAK%4}1@%QOyB7i}u?+Y^mz^w2k8Q zub#%Fx(zBffVT<9(gmZNi8TR$Dgi9NZ8?}ORQN7!>iUA)>@Da?g1?Wk@4@KWz;=X( zF;dREi|t0*z#=h6fj=cbU{$}5lK`zwF}RY5hfcjoWZf3wGFb7SF|=%ad3l?U@mi?3 zd<*N4z_Jb7HG$VL!^|6(X`(LhuKi-MCw~Gngy`iV0Ad}agMR3Z2f2@h+IjzebAPdd zHBbJ3Ir6lh>iN&ahyRgqfKD_5aZ~^v+>4}+bWv= zWi_MHdtz?6f2A;bF?^N8IuU|1%F=9?O4X=$;=mxyO6y z6Z})lcz3D=edgWGYyiRgBMKJT4^1v& z>F8`{utK1^>+ip?+}c#{B1dX2TS5FfN%5ced#BF5dkU}(z@?Z%+_~r9P9Hva@QQ`_ zFq@C&aU`OA@Gs{X`!B?GD4(>3VCbmVcQ?f`{87|NB3*n(m$++JM>E=%)*sHzLv}jor@ht~w$iCv=#58(tidvL?EP&8Tj6>!-M?dZl zE)B{Cl2@~P|9F5+UhU`)K_wry$^0W%EKcKrzQ(*=zfC6&){(&joh*`B-9qnD-&<1s z@u>YmTqw7p?;NyYULDdI-&nIOzW5I$AI(fe ziUY)#*Ax%4ZXFr#Y*7fBd--h?2~3D+(p3|=7_1rlqG+#}2rG9>u}W}prM=V>SJbux z0P_6_^QQt2#Bq2c{;N|x3{A?+D_v%0B1#dVj(cwLl9r|Q+i03LcnT5C4MuKLRU<=` zMm*9>@$&Z{3STN5@kNvYL|s^y6DUZQjp;d&#oZ5SnnJm?kF)kuBfch7TQEpEW`3pw zzqMF9!nuBZz1Iim0H*H|-{6fTMO2HRX=f#}{QqHw$E0)s9P99{t`-j?NOAyRpV9L_z z?-huvA;ay{k!-ernH*j1O)A;Z$Tr6nCGFyfHdXK++0w01L^5Kc9=omT@sl&JCEi&^ zdE-fGD(}`C1r&Sz>()M(gX7seOgkFK;t?W&hv)R8;}1VwLO`O;fbMMvgO_t4)`)lt z;QAu8w+|G@&t#Nrh?w%C{!VI3dm5I&m){qoP`28`z}A+X>DA%+Fd)DzACZb@?}M`!8vvbcQq1&;i#5D& zy@r0fs}~?7c~fe-rQwO_n!&Z(qT|DY?(+6hP>dF*zNEiU8F(`0abJhp9|mXjUCy;R zpsm>aKNvr;7teYE`UB+{=#cTNi+>U7{5MiaXa|u=hhnETr1l?a4b~SVWCDany+L@a z-;=MDvvTE*)rHIl-DDSz7RB`b=gdIqqngO~Xz&{Xy9aIpNECjG%aE^J zx^yWwR|B2lVA{cpKXFYPk3m(@fBHoJ=asaBqiD6G82&4Sd_ukp%j0yvLh(Qqz1RB? zMA{LI^DQz*FF^vOeP)h{({s8t3Il(VvNT&bl_QX1{P3;8cJX`q?4pbdrh(Wp$ z_0D9R*SlEJ&xhd$DPp5Z7IN0Mu^WhzhuMr!fkRbk){k31lHa5ay)z!Qj_tfCvlz{P*hy5BiPO(f$FB=k>m?1qd5G`Ou}}8|)C!spA)mXrZE_|IU9+lF)8I zHQ;~WIZ(VP_EZSn2_J3B<2*F;x zJBuR_86oVA{W!3`E!LU?Rc<<(GhY~7otz%x3ey#0Ywm?+!Pe`5G5bc|a%0Q_{>_RV z9YjaYTJYn0p6!VcK<&Kv49_n%RAJ4>+&j&RY(NZcNmQUGXADKYGd3W<*hTUAvL`v& zI91Zt$7V-FRe03HiNu|N3yFZG!3*UaHfT_}WIr|L zQ};ZC`Zxd8i*0XoyFf!5D?On1Q09H?=IAJqn$2mu1DvCq+reapNIATB8mdo;4@n0&D+r{;`L>XEMd{x8&z=Kj9?Dtlo@Y8qz$0<7&_6Iq+L>#OdB0-kMZpcB zi)Pq5aBEZX(eEGs^B0TLb=gcu_Dc*Ff(k(>7Mxr1q%vvt`U{H{TsAn_ucoGxjp5C$ zYFvK{oCY=9Dh{sk6O)&8HHfN} zDL2nyBxNy6PNg{p2XKpXLK~p8dR_!sq7bk2KV=;cymdaQT_pN&KvSZTG#!^ARfii) zBv$2ARq1tNzu&9Bj-YooYs)*#`m35gUxri5*nQ>5_kRe3g+auqRDG=j*VuwzHhv~)LXlj-;}JH3S8V3}v>K=i98*ltv|i z$k)WQXp~3g;zBQxN|N_PvJAp9sf_=HU7$KSS@|q-TD-Mihuo4v5BTELdB66(d7;qUxa0E%jQZOiCR<*#I*`LoHtYwS^uF2jmdI6uPCBw zBkxdMqw-VYg%&iWko)c2a98I(*}h&CL-Iq-fy;)cnp$q$4bWo<@z-O+jqmLI7SJ;S zUFpq8YH^NDDtX7C!z^V=F=OBQ0G*S$5yGow13pbE@>eWK)<1FJUDFKVIDbB16f-S0 z5R7#Pm3Wb_aDUDAfe~H_kk=x|7`Z(hmO~G>E(Fw`JSn{CjNqMJ+j;3&c%v(>#cZj; zDvdjIaUVM2C=NdT5^JnoHg0|Fb#)Q2?6xNE;*Nt+fA%^!7+s=U+>vp%yk1FII3lP> zl#+IKtev%lo7aCCSAoYHPn}>LRn>hW8R5|#Ek}gbqt;Xh*~}G;p_Ri0OuTzTLqqbj zs9`IiW|aJkaTmKsFd}n)J?U2~jOeg;I@b0~pznT0w4r~~fpTXJnJi#(p{ZDNL(o-$ z^RGdNS?!U8B+Q$YtV=Ic95{G2+{<(Ah1| zo8tdqHyb?!KEW72*q&~>!XH|U1SHsy{B@Za-W~Tq5{F*x2>P{KlO60ht4CiOQ5@NMj<-b_6@4MF7a| z2@jbpdUkf7t`w58FRa<@_nT~+2^}E*RAKDiSu@IY)`BU1-8JuFLAF>ZNv66#_Z5tS z&x&pv4ydx1Lb~Y3km+qXQla_0in>SYu=wY(>?R+iUFsubo!AYB*;!;AV5a6#6qlFJ zFd38f`)lKYb9Zg3A!*lz5OKB^s(pcH<$vGCS2{DR7aU$7oz)GcB;Vg)LziUTBT5tU zA>yGC5R)wKq^}XYq^SZ>94y50%@6(<6VxQFybuMM9A@HKx#_Ua^`;Ai+#7FP)2ZvG zWrG0c9(}MX?qyM}Yu2Xs;34_OV_vzk`wSt>)~nK;vHDj4W3$`F>OPPs`|h|T3^J;? zk5|xM99gV*7*Hp~LZ43Vmq2sI6IWS|o2fRVy!x*P;Y7F!AaFHQ`VB~4QTDErl z^7z2#5?i)^-y||cjVaO+<*yHYfjmdw^1RSnQ?bmJ7X84k(s-YFuk@MEI#~@k6p;bB z^YhNgwQla?8CN$g34wIigsY@9nu}e*1)E#HkCGb23C?2Car&nWjf`ty7ySJEup>H$ zdPL$&)6>%n3)5!EA9flleD&CezsAn?J*pev8h}Y&5kH&zko!3dI744JsfBm%J_i;B zKfK0?DSE-*HeULosWipUD0)BY&Y+PB{It|&=i4f zzLbce1jinyUgoQQAfs{7`v|%JgP~6J1wkFQ)hURo4&4KHhHVu(I~3CT17HAkNp>lH zh6bV4z55OrdIwJhh&GhqN5hQ`vBr)H8Sks}$>9Oq4yt$e?K^O4 z1TfpezJ3pq7vW62Mq_f+)4vP5>|Y495aE_-$zgrNM&&`F&u9&P5P0g27mS{2eE(QAdAV)SgtFK>+3dHpn(|3@GnETbr1)` z?*O*M7(d}rtMDJQ+MaR=T05Wu$;r$_*f|TWyE^_!C^RY`Vo!QA9JaT9qsKY zyvkD0xHzjhE~*Kq4?s;4Fd59t5R#Y@(A&o_b(Uj`z>m!WnrONyqD&j57nBj%B&|=) zbhN!@sFLk42L-UjPQ-kmhkHwXv)tOAm(zcWecD@bhX8H2Q9dnu06p9PyZ0^3EThic9MYI+YUGXCeuin0K1Ebt9F3zMK zG&elDUZ2&xhC7DCQZ>lAcqja=>QKawhQIemIlZuMdO@;OrVE%nI_v0$Xm zeg(j5g;wrLJ;>n?^`e^NmD$;_2tNj6BQ%PM4$|*ypBGa*#0(j48rr;O-hU{55Hk-3 zRN_YWa?;&~*q+YqANoWlGL1f*`3SK!LQ+4+&)+vg6pK&4PzOmoBs^$5K+REYi&4t4 z$0}dc!`-4EtB5y>SI&i><*T4f{xR|;$fg5}8(IgxArYOK(bAHW+5^`SxZ<|RL+^$9 zk!BF2fqZ(Huu;50B z&JZ~g5fX%i(jPm!KAmu-GpFzI8+@_7`~kIB9lygCmgDdb;fNunyu?2|hKU=SkmC03 zH1eAht}Aq~?OlhC3pDd06l@E4k`TeCy6PzO+MralQkbXcFM_0Y01kye6%ym>hKAf* zjQ4c`mLS060$49RP?jbpO}e{LR2ih9rx3Y%Q_QU87e``rNU_kQrb$6x+_X933BW1g zy$6zrEZ5G-h`L@-A83_GfBs1p)Y{X}xkpjS!hh7Rfcww&-=D59!&+r>$y;DlX3DHANfu-&+KP#Pe1+;zu;Sm%Ac`L#j7>skJ zC@mePbW5mgxl(Hbg<%*0@dFsS+srtAU-PNXg{(#E8(9O^we)H7o^jZkvaVop$MtB! zu)8{+k&zMK8MiD(!-WZ^ka2RnJBvNhr0gQY-LL{#mL6hcdb_$H(Fl$g$*^860!SP^3g8Lj>A%+BXb*J(nTi@iL{wC-4?%P+O7FdjnwPRl7L7818 z5=ZBKrgv^W{C!tZH|(k4y-Oiqln&jPe23ppqISLKeB{>WEq5uiTMP{G(6wC00DS;U z=QhPd%wb7n5j~mB$!cpAK#2$kIBvxYGevP27>&1N%4kLhY$%>J!Y7IUfw z?6r}Lb?QhczVrpx3?@%494CMQu%rp;w|0g9iRYToX#CEhPqvMDPqw%S?+JX&XA8q>8N5+5XUqyFq!aGTRY-6}w&OC^y|4ks=xc6gkw#D#)BhFM3r*_RUX) zi2f2+d%Y~8`6?kderpVKzEKIbOG1YO=9J~sPX259MQXSf@Sdrj{A}^8dnHsG8+DLI znVn?VXTmnNwj4;^Hx+`5mpzs_*D=7?<8#wu^X_F44LAJ(QZ2Pdi_aQRW_wEqiEWp!FN9I*YW-F9`AR&M?BAcU)S$< z&b{m+--34&Ewb&P4PiHOoEERu+`FgeT4Ys$cdheKy-%pG?iVYTbA@-7v(lCpj&YoO z*mWJL+2~Dc$HIf|0F3$mQb|Kg`rdXH7E5>_oACD&QgMgNdT}JH`7MOWpXa)#r9jCN z7iIg>k1ONt?7aR8rqafB+G~f!lKa6JIyub{T0oHF0TLUUxBBqLff1KZE@MMO zZWZ4-tReWCFPy*`H%i&ZR%3>J`*v7`oDDihz2m&v zuhlRIYR2yr^XUe29?F_J^b>@)&J!M@jmT_Z0%-z_mi$e=7AFd#hp*mkTr< zkn143b*nvZkMuBdiQTWq;(NgtPcj_AwV)SaF~ z9h(hCznG4H?9n!Vf{-K37jWZf-Qg$;z=nw7>~Yj@Vm*fgJY&d)aHWbQJbEcvgl1~`le{YmdH zKFaM*20}&5sZU$#RVxp|9WQ$5&~Ec{K@`HYCoL?(K2bMOnxE2Q!qwW`5&9KIt{}zby>TX^7MEg<2`Aw_63O6Py+xl;RYbp=LH35eJ(=} zTnrVR6Z!&tC!qWA|2e)z@S>cX2pX$(dNl59?Ce~Gwq7Dvp%cSfsXoF9fj7L{TUZw( zHv%L)7X6>1pBbP-2z;LipI?OAJAK~AQer9cB1Mfs@t{er>N+3|7+s>C~If&}}}Is#Y*Bc9p4Gia!S} zMl!M`HlRG(W;ETo+bNr-LIKJWu%bEPp-0nc@KhbzzyByp-hu1)^{)QV{_gkfDz|!~ zuEXm_w1lKMIXP@*+`erhL_V~1dnsm7$8{s%I9UR|ejMm0WNw;>?U9fGtuBk*8kibL zYFi<8#ZCh;si6MBqv4@Jdb(WYU^=k&Beo+!y4I=!NRta3M)B->YPoNyOaH}NGzAi( z)GF?juHsOUV&s~2#9s2#2H3v;gKE|H^@xoa>KEMa^4tP`gS>8-#uscTaxz#D9>O0^Bh*GdqxNG96Mpjc7Q;z5A^#SaxzfGr4Dj z3Y{yMYhWd7A05ddpE!-gk9|B^bF*hic9=HLSfA3n*~}>uu(2##`SbglgTRA8)CR?F>cc;7>?mOt%@)mJ_!#i(OdKZ}P$*R~B z%YUja+F916&RZ6@|5y(~OzT&eS_oq&fYlRN#wFPz3L`yfezdT}5duIVq`Welq=X_e z9#&Q;PB!qdo__Ei`T2ey1^IQx424Sle1$?Zc34w9%d4th_xJa|dIhJz68IV{aSMK` z6(VN7(!qO4h%W+Y#lqN?$UO|u2h`M^VKjxP^D|y(+6uefWEe}?_m$?ySGve8d-?N? zLQ+pI#V_j)Z;_dOwr8<1baT4I9j{*Tljg2?g%vpoF*r;=)xpoEzgTPJ+gRV)i>2c- zTEy+CC&_g)t4SZ}6p({%A{1)VONwe%F4oy-K0hSc)|+{`zvYL&Fp=291RN9~E&^}N zQwX`(&YPvYu&%H!|7kTx*P_mJ90VjkF2<)5{8VIME+FW^XDcnOpAkA3#?jkHuh)C! z`hm9|(Nt=N-U(Fx)K|eF5)4@Q+7KYw$rZW13R(YT(6_zU+O*Ww8Vj&5^X~IZL7yUP z4aBBi560NfKP|6g@Zk5?1l!@K3PzfhRp;W>XrDa;Dt}K-4(X>4ZQ0P)zn%+e3*|?+ zBWNsqrUvFS@87>i95Vv-Lcb=n2^R5>@DV{;BlWfDWfvDAQPZvB^Z>bTVfh2u2-(3{ zBH^1C_6rG)cc|jAz}?Ns&YmjA?(%wmm0(AMz%#s7>MOgz4b)(9%kcG)I!}W_C}`B$ zr*3T+&k+8~cxs&^ENyr293e-d(L97+K-U*adQZYxx9+*bh4Eys2 zZTxHGEoG*sWB-?K9wW%8O%-jRuBZojb!&;i?0fxhv&Kr_*plL_NN3 zaRbLaT1}w|!HR)@K!gqyS%l21%uJ_U>I}{G?uj$AjW0)-g)@ziO#!vV06hDGB#3aa zxDH}8r0EorAyiM~=a%F-`3G5DkAHM&C|RhUG1!BLw&CWoSj~^aUD7I7&17dC4z4V5 z&9jjR@LGm*cF0)uPE&u_=RB~uv5Sr(PS^RsttN~vgoZ#i8g4p@%F(+vl@}PflJrwb zfeyQ)4M8D$N&M-=xjI`WiX%iO9-7mxb+KuCDIg%+u-F^WJ)e2&9U74G} z#`2_lTix2QB}cZPgY29xBEya`MInR|Bj#zqgNV3Fs<)>9d_jU!X0ugIA|=2;sj1xMSLfjYc=4w zxs6H;z;Bl~mz)}>HubP=g4}xS_*O59svwyt0O=-RkuSFn_)L`=bK34si-EP)><%Qp z7Q-iW&)#89X!d+5Dj&qrC#n758yaE|nfFvcG4_`tWQ>`a4}cY5cvFQD|P zRWuZlLC+_D_-`x>3M0$?u_4`8O$LleIVNJt7e^Q_grt*P^54;C(-BfgW2%W+1DpqGEm&efrm!h~`R_^}pE^3pX`oPh zV1TUW%igEV^gQ!R#=?_v0`zsm6P87sGrt;vb`ZcnD1M7xquTJXL$#>!jo60yb5N4olWJu8}h?6b7iZWT_o%=o0dkB5xKR(hUk^sQZ6$s4GqB3P8lm7u%xK7bZ1 z*rTc;9Gb$K3@9coGqZyWCK&@VunigXUv6tkkQ=?SPFf34hd*j4?AZsSU-{y6M^@!V zFQx=FyXn<}-$mFB2p|kU`;of`?Fx?eYvixRWK`qCtdAabI>)Q}=Bv8OUoy7IH>oda z{n5j{Bs=xyl^J80Q+Xiq-p0?tqU*_a*h?aaFUwJ{S#r zls5%-drXhp+@@j>vduOqDy!*F!B9*HnM5-hS_{-CIwqeNsS`np90uu>oojf_S%p#$DX#X59q3ke{Ki zs>I#pibQu_v|%jc1N$WY+ffzFjb+rq%8L%>6d`|_IjY`%GZj1H@BeV(oCix~QP(Ws zJa}j4e}}N7F`fMA9i{1GsZM?CGQ`xjxnvbpv}cAOoLTO0RbZ}4U~E=O+9h2H2kn&N z=A&9Wr;Kt05qw7nbSq)Mt9GC+<_KfG*0zk3is0|DFoY;^y6jqC#F2wYPDCNNZMtU% z#Ss<*?R~Bvb^O+w<|)+o@3wK>pY84J;(KAoxRsS?g=K@m?S?0e9YPs@H!l2nDmEu8 z6HN2d`*Y9UN8>BtPzVeX-g@=EbRGexvxMr&PaY;h5)o~$#W}o4O{w+=7ET`(I^%WV z)>V!CK~MC3H;3Vot))}WHM5Im4_$e0^PJAhct=}JzCxDj=QSf8ttE7j_+yGad9imt z{LH6dA_hq^z{q_$`u5%_jhlvNKNu!8a9PWx-`0Gh<2%`H8I=&*4;PxFT)_tqKvc-Q zL$&GgIUkGd_>jB>>N_@-Gm$fN0h%Q>ZZX2M3fg~fMRXbWlbcyie;DkQv2PevOu8p< zNl5fe+PL=EIGbwtw0zIM7$xIo_sIxO(Q3|jdpH`44sVNdDdn0SgRm=jYlYxex|sp z0z1>IW|y_QJ@%!{w_MG*AnLl3{XXB1C1_~uVDhoqwW09*A}yTGY8C?~UTF>%7Ae`; zfqmkh+AOVxyp$k8imH-d8{slfP1RzSw(qx4<-aZtKZ>JVZNSeX7pYf<2G}yH!ra5f z&&zSalwAkX_Wu=U>nr|D`+KLXu5?-dvn_I;uUsDxkh)bq;KmYhvLPg~JgqADVf@-h7^)CwQnR zHiQm;N|d?AV?2(z3v-Tzu+NfXu0UF^qGN6<)&4U~OQ%LWE#9zoGO3JNI4p%CVGq+P z^0R%v-Ft+bRF&L5e3j=WpP_#iIn62N2IcgV6s;_fce_0_#gc{nLi@yYST0~5Ozvstr_P#m6Pwo(taw`_1>eLjU%k#N*t!l3lx zqm~(0VOh_z?EZRN<7yC8E|}DJ!O75gVHQ69VuY#jK3>{Ge(gu2#g^a8(>r-%H%C~g<35J|RpW0}XUxcfi~3>er_?fhE| z9n6t`m>zs2S?4>eQgItqoZ`SzXdG{>of*?|=Nlx|(q^CDZ1S*2Q02{qM;n2Qeei1~?JYrmcW(4gk&D*0K^ zeHyl}`&AhS?fKSOn*OjoxN3frNrj(tT)q5)rQqyxz_XL*F>-x%1wH)IVBzSB)h*-Z zF15|`Lo6Y3}ZYMOGD zwgz?U^<7HUfF{75M5h{vT8e+g^k%|HqdNEbGxC0*1TUfHysEt|g^>p+uO+qPwy=q? z!4E+L_J$nQ1NIHa_jmvO^7?PS0W0G3lK9kqK|)OK$=pn+<$%HZH{dHVGWv3E$A?a5 zVn>HO&ZoXy5=(TsEf(WHcfRiR1N*ahapF4oRLPR7dt9I3=_qv4jTXc3Ot#KPN&K}SVAZF3<kb-%LJ zsBX&)BZ3O_!;huX!lwg<>+me}cXxaK?7CNzqGYhi9MyS7O+&`l+&x@SB~kkmfPDVB za!)fGE31zL4LfAK7yqmLvoYAmqciY?W?jOOuj;%8L^&R{ku(se2uow z>P-hR4{r>5j(qV8dVca+TeuH@^i21gxk%av6G*lAE1!}1NpY!UN`g0RyVFOPd9Jii zM(#zbr`{L|m1N0PvTD1xxW3R#e|fr@xZIhhc;J%{3-?QA+1Ovgowk=Qts{_tASr;L zj!^b#Q+f2xaLaJAesP%j`FHivt#6Ai-ganSvDsaq3B_CxoLI2CvC)uQ3@6H4vJHCPBkt_aj+Ztj`==Si(#RjJotl}c zCnFl9vGs2oXB~bz4_DWb)Iw2@tWJjtYp{bDrRSapZW;<}a2l{`*~VsEA}j=A<)-rN z&k%%f`aKeH=^A_Xe8W^+GLNWAaA|KBMFpv6M3YGqc@9jjFp&|T=yra0M6yJjN*k3k z1s$G;-;vqJh#dIEaco+&b+Q%ZHg*K68NqDi#k@D-(VO(>+zDJ|&}?O222hG?sowrx z7@tD51fd9Z%?taaNT8CF+We8815G_z4@_uHyM6WLkK_u8gk z%~bC-r&>2~)lxqmP-bD4Ph?b}!(6|4c205Tf~C1Vo1evjm?(kax&_@ju|=ZL^YyM( zw&|7G4xS`g7G=31XnpY6CH22SBtS*?`6`i1iYY8l~_ zISk~2EsiNXNzGrqn(uYql~Zt9Hbp8S*PXM=Us)M^v|WWNGohEprAn3DBs?|5t5Q_o6y1S?jFed^g#2(uO^4gb-(EOWmz&OY z?VZ^9{GUml0#xt1xkwwCt7W@6xm`SOJ;MZs1VFYuQYAL|3*m;vy(P-AHkEu(7vTZN zW)T3K8XdqmY(4>SpA&4Wi8*e3e-f&LO>k#uQC9<@hMrh%x)?-y;WQO;N&r>sI!KOD?enDGqS{fcMkT?k9T_@*sAiACK>J#+h`i9Bh*9QX!)L1Vk2xF|6 zfLXpQct7=4f-wgF*<~t+w`6ka#l|8tEt~CBe7s5eXlDAak{PvpV-x-Br0p`_pIpqc zo~LjjDG?^*pFso4bcgb3nMC|_Xc?O>N=9D3jNnP~o#;fLUwOOf;oAxG>?+!aj7AD1 zY1ai>rh$zX4;pMy-q+$Nb=zh!?<oB`H`&3` z7?T}rg(3UIJ!v)5`8Zk%F$x~GMB=BfxJctTc@Jou=(+s8To z)S6AXG1OM1BNociG_~ma82J&NF0GN%4^Mv{$aa%HQ-7iQxL((b!^-9A-Yjy)8XP7y zk~~SRzKBbLXb7kQGN$H1$_y|i8?AUQXcOX$wLg$AEFnk?QioF^Tc04h47+mvf&HJg zFGuX{f8!HK%-Fmb})@$KoWhp49P3fs{O@-zS?%vU7*G|^NH>Iu=y~0+HLh#uW37?O6~8a zX?$jm`pQEb33|nYO#JB~t=BL9V~%}5{qgF^7eyv26M*gDl%b>p#fFs_-3IG_{(;0R znEtFpo9fbZSWi&;FUUM}g!$tG zYDbk6+h{Gs#dva!wXqi{37IIBo4iqCRp8jL~jPe@ftfd zb1c<>tfLt#8k;!I7Jz#WOuAj`#|n)NCkkn)m-!;>EHbj#3Z0-#zHyc>I*RJARyw4m z2^~eFUV#ImP1pB1?-wHXK=V{XLj$2s3!FwLR}eA3imwQgu3y_)IM+|DkP;$4GHp0| zO$@Yi^gk*$-~W>KnDhm)bANHE-Zz@+o9KS7t_IL@?$J~HM$@J$!(U%Cr2ghQI6VU^ zs!td?j{pV!`G*tk5|qklCXKkLO!1q_JqT8*qIE8s`YX+6SY8F~9jO{V(|qrZU~m#| z5FJwlL#@7Yb4XL@Ol!2FM<3bFNn~!pyI5@@fk0SMpN^>e4@Q1GU)?mZVdbW}mkVSB zR5Cw#Za0y*h)r|5e)?WX$uHiYsdLVg&KvHhmARP^?`UWGHr9N6Std$azi$^)9m~e+ zx~^5u-xDqiA&zsFE(dt^jWZAFR9)-$I1>1j@(T%>Jon)lvnW>+uaWTCnJi0w{WoN> z4=J9^jNO?%^orLwqJ}rc?MDRd>2I<)}}Rd1^c-86jf_y3!xm z(%^5z@x*kv!dg2?xS%4%-jgQ6rvuVP98)kv;bkUdSm0xo3L<)h@SW*Gpw$4IW%-Ct z1^4yz7-o!#&W%&MYkV?WBBp!omuRx9A!mt$>Xb|W?l@(JI{LP#NMjY?S-4>lZs9ms zNT1d<8#ozMlBfp6bVLNHvwaOs3yQXB(rJ-e=^JP8W>c0F#p(x0lZZFCzfK1$T zU##wM?ZCuPHB5$S>FIhE;}kCBrlWr8`vpS-Ej$0+_4%V0f0mb@~s%I$J-HJOpcahTAE>pSmJ>42IGapXDoNOd=GW{eII*!9+Mvk z-sF8)Ydt`6|6Ic3LDVI6AJUn)*5;&HfMI+1%Q)y}Ok?C)aEG@uTHD%=Iqq%S^;Dgc zK`3y{h3nDreHZFXwYQnH2QO=1B!2QnZVS0B7cX9f57L2lp%nGd0=+qZ?7r=BHAp3u zzWDarX6E-@;VvD*d!l^$Z0}mf@fd##o9*$Or9X0akFe05?0maW^6q1)0;frXqQUsu zc-6c$UK<%vW25sbBJ0ur$&=qxT4l~AYva6|j4?L$&MQJIFsh>Qp4Hd0ygPBL7gggG zhndrJ3O@2nEK2$Q#UMO4si>d;UJc#1cff<8WOXhWh~szvr(Lteb2Qs>9Zo$rZKN`> zwSZ*LLRAexwum~Afe*w+yw;N$yM@2XA>6l-QZgXmy+F1iYd%%jFv+q?&bCU4~ zgPv?9+g1TlFmz)sg)h6tQtXWOHm~yo`x(K4RY5CTc)b5YzV~f&6x0%zOWnmUP+Stf zl5o1)lI{BFgXNF@Mpavm8rJh?ztu5Zwd&&jXe6yF^K+~4A_|Z{X>S#1d?{7^nY4Jj zM|me($dB#1xcT+p<1|)`iVI}rMkTh~$5yWkii+(mQYdR;8RvO%MuY{fG~BP>)`9gQ z)bjo*U)9`#hburf@@3gj-y(>xSrL||j#&BEbtA`ERXFC{VG0}D%rp|s8G7V3D#eq8 zJOUQoumNi)!?54K{`@Pn=!P%;U&xvXm}x>!-3|O~=lC7nb3}rNZaJq~H=8(~?mTL@ zi!SE={rliY2)y&&z4Wq(;|5h9*&<-TcNa4fLh0GbmA&hB6eMp(9Al-4Mq3S@+aB=Q zKPziqAd#`6*S$?Y%tgD9X++@-=T9#jGswhk;UbjW{k4Y6;y_MC^HIk{sHMK;qjww& z0LB^qE8pecncTwv>+&7nnVK=~-$D+zh>?#+ORbgzT(aHBrxNd{2&8^sewdxO@1=Yi zk4TJqZ9bf#;E{vDY2YyvmE)0fL$4uVfQy5AoCJGa{8mq%Ad6bK3)7tZSe+*ix3w;t z9@qUSo$$bz5vo(`ME}|dHRn`6ay72d#x!H9q^5~`uSXQ zwOlpRIPczI?_W8lOrF*0b8Nwz>>sjWki?pI^_N|kgX0ErcM@ctAZdpCC8Zn(kE)b? z%P+EyTRAyKuKT#4u*}ue5-*X|`$^Olq*WDic3jtzjF_d&*V1wjVYTPrl!J)^XNAkJ zV(Nf#nZ!&A2rw!{)tXs^Q|u+9 z&D749dl=RzOyN?`m)g1)6O5!JI68tk_V<0X&_S5gWqg2@d2w}9)6O9?Ci>Cj!j+=W zMSl;GD_Jtay8yaNyRg>&9?`4jFXWkQ=O~&oFZIo>F6qA0%Odg8d08TIK+^-4pR>2c@N1Hrc_=TCN=_j4- zf&Bwo8E)~F56)C3%l;BMd&_&%HS#D!X=*)av46Fdk%FrZk|AEjB2jq?oT(*I;iva^9_;d{#<9C{G0ag zi;3V1g$bdHl3Oq3Q|9G6r~koBWPQ}aoh4a5guz=bo*?2x#2Rud7%`fl*B+uOlwu)v zmnJ$zKjy>h#%E*?1j-(DuDTCjYpi8E={zeI+{c>~@!el-J5w}}O26Df!nK3PZzf2T*XUdWjV~}TfSx_?R zI~Dh+PgrVw4EHm)R~((Akrtf$l$$;6Omb2XgNM@+UaGiv6wf3>)GK6enkBCuX)s@7 zT@Fw;yR(YF;$2f{C_3mT>60!)j)w_|Ps_u|j3YOZi4It-qJ_n_c?QBGc8jnl%B!HRwPbN@n1YFxT*GcGNY>aF)TQxZSx zqJ-MLr?QG1UN!0`rNfA4bc#&QCp=F5VIS@Xvo$9`j#JkJs4rm4;Dqg{dN`Mc&-Htpk_>> z&vSK(2P zF)NgZ{b8nyx)7rFbMHeL{m-}4hjfzJ0+~q;u|C;Vt?*FY;t+s@G>219Bx?D~52qXy zQ)!;20?ZeD)>!!GWiH-nQ%8AJ0~QY|@guEG@j+uHfo;NcGAr6&64KJexg^_5N%DD z(A;SG^XZ2k2{BML7}}~&KF?w4mJo!)2#5SEsQQ!-sq1fCmRFYQEzXVK5;61UAvbitY>0#{W zc`koadpn|4Ere}@*xbq?T1+u}^8qhsB!?1f!liI`LO;2qTq3W^3R)$9`CkC~^gAFZ&iJDYuI@iRrdrgYN>o9JsGtchc=boE#sJuN+u7f{{ z&S&BX>;2EzhmO~~Qahek4gqcq%?2tJBZj}|-XnKV@c-<_KW|-d24dcuuwUERQSS*~ zIv*>LW+iqn+;cbmiGyd~{O0Y9Osdc)Nzt>L6&=qsy91+}k*Vnf)a2NfQtK&5m}Yt` zi}eZc)q+j*+^=_ZH|ZKlrXm%F5?|4Y+tRZLrJEo6=LgjQL=Vk0t@|kU4rJ2=>SlZv zQtVjVSl#@4D1Ir=mbLsa$*dFmAA;oypX9VaT+sr|8BoE}&;Yu)iF_O~65_DYmLXkf z1#MHCy{*rdEvYry_5IDi%LpX2r}Fi9Q46Fo@_v(B{VbgyPy8l1c%|{Rhu;a^L(9j* zQZA{UK=U9tLSQIRVo6kOUTI5BlwZo8A?4u_oFHSoQA813pAbrxy-ZKav!()Ft8*Q+ zSUF!cjcBbN&7ohp7Z%)S&;CL{@ZrT(HI@!GzUG0JkEfmmTtNABil7dmG{UyN5_iTn z$*ZVSy6aluqr(@|_sVY*p?=l>=?u4a=EVO^ocJgl6AL$FrStLjA}{#ww^KU=WT;qv z|Nha%4K%M8yM{4w*ZC*drYG8G7>T6NlGiC#n-wrxMcxm=q7%S@v>d|c3QL-Dc(BkT zz&{zr1*_c#wE@B@zTHWi{Ne2smCuYyv5@Wje%nMy*w>|YaBk-#?ECkQD`jlItbY^i zfAeG9vrpW&wpBvaBclGYi-b^Q`8!vsqH?CjgG2V*wSx~Jfdk@LQC%%2<*3C(aCp6b z{TiJ^PL_g#hR*`)jvczg$g5POmbV4MDCwqZSCGn5G-rnaJ&@>ZBT+H;V zEk4+eay*DO>oQy#i4cHWE_?X+PJyQOwpBf>tpH-)|L_lU$IXK*W*0^R)I&(*8C!H| zLLUJn&%D$nqFV!vbg_D)#Py|@L(Vl{sN=fC+q0|S*+s5D$JugPuSQ71J8(G&3q1@2 z=!@`&1*4V$IW*q|18S*4;;jBjMO7Wl`xo6r>}ufH7Ji?*$3(6S^N=$~lq5CfErlK; zejRK|@%(Y75l(=KYTnY0Y=uy1^1cBTJXS)dRc6+YKKqec;6W6*`l4X)8uiDEFvNbBQn(0vOa!Rjtl+6 z0yk^#F|S3X(tPYBY>|k~Mi7M)EYjqOOzhDv1%sY8)egv1?ZLA98;`5bY57ysCTZn= zObsSjcDxF7&_<{qE8#bJd4-hnr8$wau8wq=Y&f&1{UYA39z3MBWwl@1NazEOVo|{H^~m>= zQC>B$5q3hGmV2w|haf}>N*0D)zdMO2Q2 z>)mqADE+yiC`=KVa?SD%$~rglGj>JMVua+F1HA`(%K86_W#s!@1*~+Bwp==2_Cl=0 zJ=ZiFmSEeli8KN9dG9E(Sol|QRAu0qHD^WK6Il51B5`tjvMo@m` z4-)Fq!hoRGpW7)haaB3Fqu0rwFwn+dKmDeVy>8O7HeNFeRku7zIJedkxfQ416^ZR$ zrvfA`B`r-*ZbN~(hl+%%&e1uQN6<|>PjG@-E0Fk-7+WHfj%|m?BktJ(YbivKBBPJs z{{sO*^Hsi+N%-4HMLHPlW7(sLlkqI%bk05|L)6xSL$aezom)WgEb|0TPfCw^W ze0=*1O7&iFOv~}$g)hMh{7avG(>I1%vH_5xKrYoz>!#+*2ty{fPOg|_vpuA>%khaXqGCQh zAkDrS^)oWF%%*Z#c1av!_PIyRzm#+Py7;eDA+EJgF6X$=rBJB#lX>XdZazJF=# zP5m>Ot764TUAt+lE}z*ZGC4FCvX_IlN=NMRTFF;8&Lll&#lGOV21AKuy&YFX)YT+< zJL3#~U=NrL`^G^`-S7m%qjhxba;&f_OPn$<16~}*mIzR|1W*tn#Onf7^%;w(dD!e0 zzJ7Z!%DnrO5m|)}qbdA`mwp$rktC9J9<`J|$DB%1Woq8(WqF(4L3}gO%n?0>sKp+K zw3RN!0Cx}qonC!%PDHH66)=D>&F~=Z{yMZ z5h-wKfIJH3{!OHr>%^uQI3b`7wmr0Wcfa{~@0{;lazY#ga2CVo5nEJ_|EAdZ=+4}D z`7uSzvU2p?qYsX91gRvX3z{ZLzJo;)&T`CtL^B7zduRsK_N*A_Ea(pzPZLFMx3EhF zyd6KHEEpNF;(2R&@DmhE>&O{?fa?e>lGxR(29#yv6%G0;^Ob;C{9>gZv2iHBaycS*{LLgNzR=*e|A0kPBzwBJW zDh&5gm8mNa(LYT-`pvJdZ}0BSh&w-Up0|>*Q$*z#Pm=rpQvxYc`5P&i#dIottgDH` zC47#GnD`2R+g z>GC#5kCHp@iq=L)WF2em`>WGqx8-L2B@5kel4wa4k$MQEHTwlekgE#L+nW$Ee(PpdRi$QSVZwNmi`F%_+ZzZ!TSjkV0I2md?e;BZIvr+nQ2|L?pQ+T~Mg(pqlxR<87Idpai~B*h~? zzYeRw9x4zM<}2=RQ}gx;TBA<|e#(@6zBUBpzGzT_MxaHIjo}*1DsFC{@UP5nD_>|K z=M)HCM+5gkn{@GKQ&HVyrDO`KZmt%a`&-qF74~u-(_4B-S$Xlqs~5T}1(>3+jbfs_ zq8ZnExf`+jgve@SrTZfoYq?r2fXQ*O&^qf4PbjrrkWL1fX&9!}oRs1K`tQGyAr=HB z0&}WvR@(Fd7-YJm)4mp%D8~Ser@rzg`|JJp$p|>t$Yx9A2BFX+2Um^O#oAg*~wI$Dv_XGk0QODB6dTWLC%7HlTN7zd8?GO zinkS*$%y|w#cd1M0<}lh2=A{yc=ynhiPMAWjFO6X0RFV6n);@wN)TIU>5o8RI5HRAPNpv zJm`dMfnapCZW7ptE{KFo8TNgwCB)y!3iIE2Q+zX&R)4zfq8Fcd!Mjo*sHJjlFl{74 zL6BPIumZSl5HkiTj?V`U&_sWPIROhhMw=s0w_q~D|IWy9Nt`3kr#H0&ct{og^<3v0 ziOKT^&4=VXc2ykuXxrIFQkQ7bC_sgLPy}uu&t@IvO|~{RV(~&WS~F6sSJlXqdw{Lt z+2VEsz9%7kw%W?p7HwMF!4%*!U6NIOCwg7ibORxhraSUup_PTxo|BL_+mKhZ;M(CP*)t0n_13o)jV)(?>X=1=i+)p-<%3KI_sk> z&0!3o%qakEl)HlmKKWX{A}R?b>e7m;D-Q$V_gWqL=lsipBqq_>Mco`5WgS^ys zz5)vg0Plzg*uYh8F~W5j7G8+I@VmSi8nTKq;&$=L+q#mHM6o-u-~)g*2p}VB!c!^* zP)a-V-Tjm{i?6%T+Iwer%L{p*0-iEZ@cK+qH~q82H(x<4;RmUBO7urLuA4-^ z+s->chXIo-!5d|7@73@QTu1CE>FGH!ev%b~iVSDOR}cz+(**`>aobhm2s(I>&xY@k zS({<^$G8vc;s-=*J`gWe#~DU164A}-BhLGWKT`3Z6LOcZzQFI=Lg!}1S8rAE?9+|o zGL6$DZ1!#P`Vl8?;49nR@mwc+*{Fw?N6Go{beAp@S$_Z9cG-P`q0HH;m+tnuMoEn=BHOdZqX3hvjl#{T%*>i0J7**qQ( zN?Bcog)imbc}R*R8Y};1Inr_pI(~&od}@Sh9bDy*DfV(9x3T>UxvAM}6E1p7Hsq!f zG9565VChKjX}>m?(m!X)+sXCUDMeklOfy+%>e1iJ`K8+|37;xRY5zSvcKz0ow5$DH z5!9`1CFe+JD)gSg=|4*vkxR>~`h?hUy3t&>)_XHg+j0yz zUR~j?iq2X4`PXXxj6Ih}R2;9NCBA zqEEDM4vqB#Z4&02fZhr47INx1IXTlaMrj)$l4W zX@GmP@8ZDo0P+P|b~*}J=8SgZHONg%BRQ9+Dd1RVi%QQ)40RfK^lV9@N|^Kpo$Gj^ z%14#(ldpFx(I_YTjpg=I2&)jQtAcHBJ**kTM=WAktN*Ci$yZICcvvTCQD8qXjwi&; z!vmdai}3Nk2U^KqP~?#%5s#CYKoFGPJV~&>LEjAUnsmzekM>!o2V>ua3XRHY`sc5p ziwg+aRoll)@zfEzOP}#f6)3v)FpAB5K}=x>r|mPpZ`m(oVU1fsh&JIghA{Kqsiu=m zWR;^d)2eBs;AaUDtR8)Tfg0hGzGopZAkY730xR$oy{A<_+qggO1dQT=$)3nO-;Vtc z)=4JyR>-csQC^L$29xCRdv9!al5T6ViCZMdwNfi~PWu(glln4aBOxTzV(f62=CEG z1x}yRg^e0bvl4@I^rr8+4Ol`*c^Xm>#6Xll_frsO^)10d!VmeP{aNyn5wQ^hL}er= z%oioj?KeIf5lQTqLB2{>v2I!QR7S)L1A1-4;HB;yox>n zj?Th#Q594hT&`FCYmC7_4G4oLsS_9ww)nds5&lDW$PbQo$Hf#*Zu1Nuk&@C)gFCu$C)hi0 zp-3gwxa!`hT+0G%K&Tk-#J3HhkvB77;>Kf`R4|$o*u!Y;IxL9zkMHxf9RG;}0T*>o zOjKv_zv$6+x}lqh=^~038k3?T#I6K-UdF0J2IeoOS2u;dJq;0i6CWD%i;2w@;w@)_ zn9I2gsGFwDo$i6jhkJMjQVtd};@f0i51#xad3I4sKs`(P8eDP=5YU$Wqiv^8A?}2apDXQMO8n&7Hu-?=6#%E|vsPN($> z9JBi98kH_9NwV{CThXRmi{&p{d;P$nW9x61G_B7Bp;VLiwEXii+z%Qw}!;E{R^%&~4jqCft+x#KRG-(b7mMmfx@KW%25@0C3q; zCe`t@9UnMvk#yM8GUwdg=e*@B3^acuGw4^&)Cj`*a|nPGCU>5}pz?w}1KbFe#tc{G z)JS+8!HM=D44i6jYRpzKF(&!bP>_L~9s*85#_q%7rhii`O_u#sNjONg^`?2_BbDec za#|au%a#7(4H$dC*ufJonLwlx&!<_y_a&xKxY*wH*sLQlw(4!uEViqFYdLb)hA#6D zQ#^3gL_&n!UPfV_Keb&Z$B*xHPx@k%a{c=E=D&q(*HU^o8p;2l2{+G_Yj>5Sh}8LX zJF^!3*2k&lJt=&u*7J@6*Pc@~#0jxHwyCIx7v`6VI%*O%{S3W=A%O1qE3fmNZb_9V zD8{OV6Ta_r7`h*tc__PIZt1#HY@bZB+w9UuSGM)`UG@RNj|2~h1~S*0Kt2j!4^xKf z26rdFz7}UNBd6MDHZ{LL%AXnCT`!!7&n}hk`JZOOK{;W=FCIw8^IWwvI8fZ@ieIs8 z?#VI(kLTad(0>_(8wUBaEX#$E^M&I+{B~#PXKLQn-rsk1;r0O4-|J3CbUwx$I&9^s z%W7?r1u`mL1KuBsd&epMa{Mdtkj+13spS`6{z%2#u~6Br+!l5w*nqqS1sF*lFlw)H zss6g|eb6cK$WGg5yg&1npGo$J$a=^tY`IRT(aKVH#p)JlZ{xGP-j><62$~x*09~h43~)f1qmhUiiz+WF%hT9x{7&i9cZ=%xi-nX8&|x+qJHnI z*wfm6x&g@=r(V_GD}q-a&09#4f3jpsU}g7P`s&2~KtEb|NIQ(Xt0VN+W)-7Mpo&PM zm9?UU`0$yO8%(jCNRosV^Md#%XH(0$JOrEegULnEaNgodncGk!9enIRXLvsdz~H}M zz5AZ6JzExq1y0P^vVm-5RPMhu+{n(DosyQxi^rCe4MiKA`z(vkEhrS{_&!Qxt*7DF zW$&HU_2=-dJQ&%X_$L2stjhypJFI--6$H_QsUp3#M53Q6!&RKCJah{HQZE5gz}eZr zWpo$F8%9zCV;WAb<5c?YR{LlDRB_Qi!sHB&8SZ(e$1*QXg+?L`&f~jIL;Z|qP;$O~ zgfN$c*`6eW`0nIZgbaUt^`t>VvFfB;OO#6K7bJwcHM&J1@9VnMq06ZJ*puk^(0x7P zN?TA5yId&7AEIr5*y!egdW`XY`tBh>4Pc7?vVtp+zS5ydX634Je8B|Bdzm%VdA~=2yt5ez47=cHh`FZD@h+atqJC0t zH`pPbfa*?p%xMJ1!9dkKy}V*NXW+1EEEW+bFsrA}N^)HPkMPad{AY5B`}gm!8P#EA ztktR-YsPy6N1I8onau_5#gEC&KEV3cy0Hs=hWgB0gn3R%3N*i_Ci1uVDO06i2zm5%nrA~ zjca20dq*0D{|3+0IdXo!thwHgNgr&~BL7oMGA=@$U|Wx(i6V;i0JFD|DVW)j-pmxc z`b2NM$q7XDO!Esjvd;#v=_-A6LbQ{4rlcSWR#MkQ-a?+D=9hR$a+FqlthSu+bF|K=SUX79)P{0G!il4XISoBAimd?{M7A-J7^WMy_Gg#_5A(w)F#j@(L%dwiPiT+ zTmtdez-RE==PACxFw`aBR9MQh#zSe#iFh((ll^B<_6f<$5E52je)6~HWPUGq9}QHR zpk^zzzJtN{IjsA?2m5~q%l|)Gh--M7py*}N*IV{2Eqf|>x{G8n*nnZL7xjD*Of{mJ zMdj7mSHR5uHeHAYl#PpMg=}nWSY-F&Rl?jZu7%`{5e^XMkJ%fIR@^Ldr%mbO3!%?J z!(!U~JW0O9Ix8>lZB&|)Fs+;PJ@O=(E3Wq>7-ld_gF}D2%8bp8DpE<7ndZLF%LI8t zIRg(PjDYu0S8$LI+S)RIB>!;&9AEq%^6-Fz;UFx(N{Wjyd_XJCK|q-^l3FHDi$lmN zH2DU$572M_a*W^DI_0%;INdUyCsmRR!!}mO|U6BuReP+RVz-`jDhq-*#OBlLfr>0IM(`>aftLH}cOT+w&)UKLai^G&ID_ z3oHTJhDy5%mnx%qnrJf+^({oU!_oE3qd;A-oNv#U!DHT@qMf4e`p!lj^-w-@SL0e-sLP!q(sS7zwPLo; zhC4+sFV6m?x3;4*iOP=p(YMI#6BerOk|yCVaxrRM@old}Y|*gfYFU9L&5Fg)V;K=z zcWFq_i4+iQzFk89o(0gBNz=lswE72pOhmXNnP?&0ou}Z$`emrJ^vV^*-PTy@w6%4F zm3c40ZF=^77Tc$h#Zrx5(~*>vl$a?H;Q2wNiyKl<9Ov$UwC?JUb0mr;|5mRY`Mcw@ zMZOslBoOaDvc#_z;DJrI*57Xvi|xqodQ>%oK3Z18)$WQFMxB-=TJ9&m^p-oxnQ6plQ$?RP9l9 zr1fz_L#BzzS$-lDF71ha^~s4o(JTfM(eB|k@r(0)EH7TrMT?*Y&X7nHFYVA;1|2BC z-(UW>1~vv>bvF@-4S4GzTRV}kjeMUwdx+n&ewVy29=?!|>#8x_jN+dSfXl01eqe6y9`Kd*Sl}S9AhtN=i!l zJk=~Jn_Nk=_IA7@HAy6SJ71ybip4{rul+PmHMx=rfcwhIKEzAp!A?6(@~o?_?{yLh zF6Y2uMAH?FEd43~{wSUnuX>!GoG^OUyHPV(kd>!tl#OM~K^a zcJ?UUq0~gm&&lUjUK7nbm_mcld6}C>zv&TUqbLr>RRhy9I1jKI_~Uy~$#A9gzPfuD zsmE6b5MthSHlA|=D1@Yp478`QVA_Oz!gtu)EpD{`KB&ji)Ih~Qv%nlkuE{a$kAwCP zY<39D%?}Ey@+It?mP61y6TGy*JDTs%aVF_2ed)t8mT17&b6JQaPr7`6vE}NSYGrN7 zw?=M8oaEu+rH5|N^(+h7acAj0jhzo_(%RVlyJvo3El+OACF|$YNC)p-thcRVB913; z1c-O^Qhsog7b+X*s-SLS?IcULQ{{gQyz}+LlbeukP|vthB#{{H+P(WP5-0A#j>)NH zOK=GtsN5ZdD&Dz+4<7wMi=9UsqBe)`9F&59#8MJD(*j%^S*w?yTc`m@!*Qq3A<1AZ z;~Dii>*A8v_n@upDy_r`37v{&(*4pu4n+t-fMUgtEmv?LVgTV}GY81w@7CrQ7;0Zs zVqg#p^OuZtO29Gm3}3DLnr6OI78n zoOHy-f<;qk`wvVib0s7fiml7x&?gHB7#0YMy^KnhuN;ep^L>r5K*pwb?(0wm3q=7I zo*K<(G0Q4pmfgaShIF!I+VOQq9s!NPyUu9gT;=VTx_<7 z?g(cB7z_hZ66MPuS*rdAha%MImy7F+`0$bWER!JxD;p5pkV!1fY5KZP-|I|@u*hEN z9~W!gKk%`S3Ui+WWCw3?vd!?@S4{>ekl2OzDo{-b^TX6ggXVQ?6ut%xcq!Um7dq(O zB@!A$PJi*=zl%K96ma@+^}tGq;IEe6y3G5Pre8%!oKAZVt?WMc9)`f}?JEXFR`-uk zUz7NpSanh~zM`9ZjMd>B!%pZ}Bho#LI&7mOQz1;SAyh!1G~!KXOk^>4nohj)(&vHW z6vjX#@D7?3xq({9R|y=)=dCe@YaNw#B?4^XOfbm#PD$2n~FN-=8ic zll48xrtp*7W6S8diL7weJq%=_4a~vr3594cSQv{Bi|pa>V%t92>705dkq-k;pHxrq z^;)x?R~L$6IT|}uHiUCO+80nbT-c%X zDg9c<=s{zO>KVWBFF&z?x1b=P(ml0qRN2IKurX*s782bi|@tWiQC}S8|jHIUzNT5Og#@a#aE~|Z#=Vz_Dz!i^)E?2eUhGt0#70G z!yx-Bqw6tIP*9*^;I0OPw})F>6AE|q3W&aNPa?fb&UY(M0JVMR$g< zoymrEXi4DJ_`*ZMK!io#px_=@CFuRW;PXaBMFuS|Wh?#@wm-cbhDF2hjw&uK6%E|6 zKVisQ)T5`ka_@`%I3A*YB6ldE0A*Xj0sF?fkDTG2@qlry0RT+vSTd0qHyvhZg2DoK zs5pM|!^I4F0bC|WKrLCvA?D)bWc^H*TIZ)qlu`6-aLG+VlzMHg0yhKP{}DQsE;FsW z*w-QS9NsU4x3spnV);2;b_25bf}B}mdW7ood6puzp-PHi@yXwlr+o!{ovQg?$lAKW zO#A86r-Z1R;0SSRklNpp+#gB12_mOOt}08Xy|Xi;0WAqDhx?w-+QTm05|f$5pMsis zLQPfMXw^h{_FdL!IWjIeZ;1y2-Ln&JIZ)i6JbQNU-aVwmqqCsdXT#;6{#|5&h6b6| zu=O_7I1V%jToD@zYQcLed$~N30k#g;<1aDi}5fxQqTNoT4r`=AkSVV8G;ZK#i2Az}896A;tB6O%B;t*#svm z+Qba6MI-ylk^}aY4xJr(m09dVl{x7Zjrus* zA%FL&YL}|s13FWUq?h3`h$Dmt8u_1T*oC=^`TXcnH9C;;p~`r>^%o0R(Xkdvf*Bu# zN(%xE23CJk>^v`_=#qGqKZ$-+h zjQz~A5^i)DTnuy2rfNKL2?d6La6qWM>yM9kffNt^6Ukoz1vx-dW{L>P!_ABIzjx+) zrbf{j;$i@?hlM?h`cC%)`p`oLYfF@cb?#1DrAGR)%%bSo8_AX=UwoVPb!b>2pMyf$fI_o<9mK zz44I)9}Wdk9bE8(8bJzK^Wl~osr6q{r=pAEIHGhNaPeX#Nr1>8M*PJUjp>eC0fg|c z;};*jwUneN6q+^jl4m+R*;~G{sjkpDUt;;2Twq{MS~5O1C`~c}NNgwqLtzyjZN^*O zYx{(gh5~iR56JEV@8O8-!}&V}h9ld(L7C8HVt(T0lr49;@65GRI>uxs?mW4f{!e1< zk13q$_;J2B?U~Se)&=U!eM$#eoMD+4VUuI z&*z$vVP$Gc_wpA+LPf(cr3F>a1qcFoJ$N;@La)bV5~%60cOX2J;fQi`FIQ;yDSIoc z8qoC6;y>wcnF+`@#Coq|sF%|$MRM{ZBp!r(63>XhzRN9GpF{{v0;C$$Orb3ohOn#? zEJqIIixCKM#q)t-UZakrHT&py@-$01*F)iRW78K!N#Y8uVA^a+1Qj#Rm zv=A(Vr(=(;EZCw)o08Uc^bM`4xkbDvzI_}1(dXejg49WEI-_*^D!0#M*G8bHLdPuQ zct1{d#JT3E0i0dRTV>MvjD9Z`?z5T%=2 zjplLfJ&i*S3?odJHj0+y+vb~hTZjQn+0^O2)kizmlslvq{RUw6&LI=C!b6Bp!k6Yd z-B{Y+QgO8p_ubeBXi&jexMQzL7gM*?`udy;?kpKee3MUglhv-!6Vw z3LrIy_$-0z0h5_OMp2$98z`S0LOcWzQJ%)!YW?a%6h$3JM&uozt|W@VDEk<09WsqX zr>}yZ^Qrbnnt$lZrLbR=_+SBs@IYgrfRt0lcal0Q${Tp;)+b_(gqN-pk&N!i_QTcok zi8>DVy;I+P*$$8;JMOhFoURH{xnTh=)^2&s*qOEFh->Q6;1sC?o0Jg>nZ9Tr0~(!R zu8;inOJ}Zn>&~wM%oUdBzMh>xe$D-XBbYL8;Ut`%V##pxNXJ zR2i3y)%)8yweU`KuVkWDRa46fGZ{MN^I!uA6|5C+>xQJ$E`1M7uLZAZk ze%$VU&wqTLf@86l5-+JTAxy>nNryv7Tg~IZ8&?+O_wF|_< z(Tj^5cDT-UOJMda&#j8C&-*XQuACz><1h+xsI)ESf=uIhH&5Q(5S}Y@fuw>VXr!U1 zc>+@s{58n^epO_&jeUPuZVXv6Sua<=STdD;A0Tcp)*EqLFm%v4{pQh~Lty>NIX#`l z#s6sL_%yqi!b!yP=qYSl99~JP@8B#qmMA^1qopBuW1|v#;JbE&g&AQL(O4R+`|Ly` zdelRA9izXK)zf9t;{t`*5aI@Qf6z}D5Ht6^vp4?{fAI+qk9Db=MzvN3HF&=8UW3-J zg1s)TIkGW)EC|FP*;Q2C{B%;06To|~NV#60Vye6bX}uv`co1hw%OwnIi_dS#nlfui zO1b{fEwDsZ04_*)1{qvYwXLL0CU1)J_f~p5x_uW@=YhMYIIN{avzDJWgd~1yJla_4 zzOUGo9a<>K|BzNasYlz6DBS&|-wu>B?M()yWV;GV{wL5o2QiH;o9tw3LY6SX8O>vtRZHVFD%_e{TdTCI40M#R!LyTBkS_)-ML=)C&-y^ z3wkLoCqL3OIyoRE01E*-AP$7u&$sPYWNYlYEJV}DFaftMNj}I1*;;vWX$(RzD~ny5 zm5*vUY#eKK`I^%R#!IX{5xKd(lSPLaGXjJbUECMZxcq}c>9wy)!_Ba0*P9SiqcwPh zHVrV#Wq5#|0!;!iU4GEGhe9}0stG6gGhaaPDn#|dMfV+qsD`Oi0ZBY#LKFslyvb;--t1L01Vls8fBWz`vF{ z{_b~<#(Q4I>tw5Dw;3w3{)id<&Anpb#C4x`bn1vXv&|1e@Y9A@Jc3QdxbH(yvf%jO zJ(Q0H=JeZ(UnutqtQoj6AcuYdm$d#4-7@a3=WaLM?{(Pd&Qo5LWTv*&WzeR6&mkSq z)&1$*CYIe7aE|x<6xuQW?+I>1YHOvgx#>)_ZdGHO&_}=sRnUv!sN}IgFP>KQM2EHF z2;SQi(`h!0aqt}~E5jDi4*TglUioTY^>Gngg3Tz3?bX`Hfc+7Htu2;mN_p?r?h}?m zzBpTmPtr+A4Z(Bt^Epgi2y2y&?4d{8F5<^^(+lO|^$j@}3!eu+%KIfP$x2GWY%G#u zPD>Su*X*l;;ty3hViNcC`3M~(uL=U&05(AP%jQ&iis%IXB=#>Fi`Am(2|f|j0}KKK z7HfzQc4wyL74Pq0k&BVj(s(6=rH&Ay4cE9E?fTp?JM*-2^>DPV{OPv@v`=b5lUrw9 zo~v(k|jB^u_fr~egx|J+V zvS{avp7R{X&Sr0YXSqB#)1D%9K3c$RqCi`SM3g9zaOEJBOKGbWaet4%-4tn)24^h3 zN9C3he(Hp8Cm`?fG1!c=#uxrVtw1x)IPC|TkI+h+{2$wlc8B9VJ)ir(=_N!&3bJur z9aWO#O^#@I>!Bbl!I?9Y?kV^&i}cF!t@U?$6WE!<=PqB3@wF8h3E8u2;F;j3U+hlc zh+>7%FhAcYF9dNcC~_Tl(Edy}AGKzU>D@LGYNBV|D$;g@B{;FfUwr-k*7L)m@d7-; zF6O7*pLIBXo~fF4UoSGX^!QW&6}+!XEJBg3EAWK;J={T-5K^SY5s5W2zY|q{wto9ra|w+pY~;S;_L{x#XXH-=V(V?wR0{lHSx8ajkMrb}mG=)Q(r+f>DL19HOmg|xVayLnlN!9qKn&!N;TNwp9fgHdl48fV zLg~vWzWgJ_CUNi z_NOCD!9PCzCcJ(CS%XPVtrV7T4+0c36|GAfb%@q2FjrrcJcT$Nk@7E4+#>SeHj}RJ z*4Umqj@I(<=|>O1BVis$U|mGUIrlRo1<#;A#0Y)= zI+^qbx!ec5m9)MM=@BH4XkEB~hO1S)oExuWHTG33L+2Iii z+rEE)6wg+D`yJ9IaR0>EXRO@Gc|lPnO^Ph8XYTxYLB&UopXnV67#>FZIxj}D$51@d z^rQ3q=|)Gg#J>b|0VP?8<<-w0vs1*quxji9@D zf5COYit=1X)QcwmSM z#tsTUL3X-s^dyN13HhY)Fk5-|*l(Kc8gmvg3Sh-oJ>fef7$GF&d;t*T*1Layx7TJw zjm>K6@$?8kl&*VtFzgG<-pH&2(gLu4`tp-JHb%?LewfT?$@Z{iYaadGE1ka@t{D~; z=x&od+_eOx9Hn#vg+bem%t0^n5!x_>^WCM2Bpua}v-x$PHL2+F;&y6DR>+jKSOF1% zwpozo>FhnTPU*F1>ya@n``VSnBeH6%l*>E7e!NRlkSVr3pAGPljZMu85qql@@(aVA zEn`>l-wfGh4g;2vwNxX)EZo(22Ed^p4WLqDxG2(cq$d-08tON}=s-_?-|;S9ZT>!; zZBe?yIfFN{vx5Ooz=~|-ChmMOQq9IYP%I#m%qB_yg6!PfySx&F=eEVb~46_ zuUOILvq^j(@BG%I5>-j{?q6=~i|W@rUpzNy{0}qmtFHE?ih_MXqVHPidUR#HhCEjl znC>Whnz2RAhFrD(aZ{5O8y*z}lhHEDBEp5~F=RJTDRp)0nZ;%}1w27F2~K&$q-yuW zvLob2n8=p}hLs*TGJg1#W})R^XV=5Vc5dl~CqPOFy3D2DL@6bXUMIz!gnumsvNo1T zCcsxqR$BVga1r&SoVU8Jj*ZDSjW%@8Enr+wQrZ6fhyM-dEC5}6Jhsw@qz!I8&$xN> z(wmd@Zsusudonv`yx%?6)aEU;ow#~Vr_JGOHP7^O4_h<=XD)W+=*VFjG%g`XC_{a3IWrUC}7uBG`LjP3)Cg9=ioab9jqB+w25tLtIchwW0>fRV~FAUu$;>hSih2y$QSBKM-Pc zwOri>=eJW2Y4cLn4`VolZGyCG<`g1K5zPgG+IH(I^mHsD``_Yh6)U-rFR`xWwp%CMn z>H3e^DGcW55E3;pKPl~2D;0~%3C^-BzDYr!PlUOXE}EyE*{Y!ZN@3$BuVhfYn7%()T^l`EbL8`!e?+Bn;VLK@;q)!mA8Ni>a+Nl6WaTaqYTJceom< zOwqbfM3`b93--|OLaKr12x(k=|Nb%brI-YSKK}vSw*~p*2#UG{SN)qufYG4s$fWUQ zZSk*o8H^-gpbl9KtS#O>+&f>y&{lnn&OZ6%Ls+m6xl4iC%>yu+COEH0E?u{9Z!3rL z1VPgJFo4RYWPZ(*#`*D(;l0a-=SA2CLQAaG(A7h&?u#4W&tYa#B*VqZO8(fhTp(3R zyvjakG-{=+i>B;32E}Yu>}1T2>T9$gg##&$+bV#UTN)QwtqwIjrZJ=@3rF6}G=6s~ zbEQDCg;*YHde+9R)vyo|POk@!2$~OpkWKu%X86^VTeq%i*0Yo%^2j>0T#8)7f3}N~ zo|A}^Y!`YK+xP}hiO0l*@y`tFdsev?WOiS0>OGQ3<8Q*%p_%6=dH}SdxG0AH{0V`E!p@HBrtwV){d=0~ zu8JYuG`la}SI`w;|0YoQ1q_o6=W{%H*}R=8PneQT{Jcf>rIg&^z3t?2oO9!vG@m-j z%(gHe*Y?W8_0MMfgdCFhz^l^hS?aIbQ=zz| z9U@y4Qnk3YsJ8Nz(LaMGf|)56c^!i|N#stV#_Y5()WJG-J4~gt(%Gi{3VA zJmDI~Y1R&)cb)o!tr<4xo8axV`;v3)Nb4&RP3?no^H&g8=psly%KDBt-o0V5y80#B zk!rds?wsb2ad&j3h0Z4BwvN&_U|zueXg|U+j18`!e@!!SW|DLFCE!+_$Sd3x_gh*B z7DgC_%}ojAsc`?8Io!9Xtf=AiN78+rZaqhG=nULn2qw$XSR(?1BXJ#YxJ4x{T0&^0`(i}RTicd zZiG&X@RdzA1nYSt_jkx+fX)<@m&bpRHn_g%C||Ml|0{3x(S+}!Mi|GCV`iD8dy(Y9 z|Lhto#a*|t=5To?&y(1wX(sLa_B^a;FUlB^NB&4^dRtoP3v3jT^C<%J#Cdvcf5je{ zIQ3k3Yf%Flf>GPk=cln%KNM=lXxqe|;R#zj%V^a2c^q-gfu4XFQ zC2_}6P42*!B-89Rk=h0BD%p=q(jq}S{Vv1I<|l}(1eF?{274}Rv5RVwyuNSwdHTmj zL)4GR!)Ur&7^8JR{B3sYq|!fTNgrnHk9itDqvO3`U26i-$oYBEOi^*52Ou8)#f61- zpN>SmMrqTYqbEEaG(Sup+{JO0gSFW1+=CfQpTngOC=F*`3#j_J0&?zqY#HNFVr`DC zN=FoJW{Ecyl`olmBl9~(QU*_s9e*BpoZrvK(|Wjl!_&IN0t*F@G4`B(n`BKUe&Ly! zF4IQHW9Y3;f&&V`$EbkWH2b%&T2)#N>v3&kFcFIh-UDtg8jt*+J zv#n*Mz4H&lbk_pJ95w;f{EFo|7vMYuPuZaing;z{dff`5*51fMh6@P*x1#eqe%OYo z6ZWzA%FRuhWV&Z8Z(yCRaQS#vi9t-!z-kF6{kVS3L)r`}Q)wDWVV4F!Hxm=uvc1z5 zaBTpVI0Z;#{QAy&v|lm=8STilkboZeY1|*XK>+;A;2`{GG-ena+%(~97=cF?N-uod z1K>%-?tAkmYREj+W|(I;03bref_X}$!@Q&eC@Nl8gWCsz@VROhvHn-%ecq9ZiXqpT zDrmdA_-5XO&r(@FVX#foE%<;~5g0(g`IFJlAfu^ew&L>HRMcqePiXYaSDbTZ9&*}ReQlJeZsT8oKHb`{M zw>&H~&R8wMd`hpJ^=nAmBq^8DM^QZG%C}sl^%MwCML$|4E!Hr3l}L+y2iju=;vPF6>>U ztF5cU+5C$6um-fb%nat*1f5d}@-)Ej~Whpe$?R zo@kc|b?>!@te>~prJ0uW4-62L*UlO6Kqz*wmR*#)3@$e~v&1Y2Sdl<)ur+BoTA)104~s{KLze7sN$OmQ!Ux#mvPnG zjjxt7tqus9=?Zk_bZ4-?)4hXCM!w`8h$=k*!%GUsG*OU#&y;s}jp;Z;^^0dCTH>Zi z0kbPaBe3bl$C%vt=W^?t6^QZ?l*i8m^5{eFVX~K&s&(B*;opkwLOZw}vNU9IJP3Jx zbg_QW>%?#&rRUi5vv-|OcBE~aQOWN@o)&4{@ZB0{Q3wMJn5>$aT#Q=5+nb)>u_5&o z47>h+Tqor$rj#QXpA4D-8u^#E~UL@uJ{-$b{4SOCKbH zfeHu*tGLNXPwmnX(nAqXts1o;(f`G8KE1x<`BEPZYdX_cRiw%@#RB%r_Q4|Q0~(p_ zwhpTNF<-)`ok7^Y*DyLnFKJqe4W$}-*no(PtSNgb<%~bCn z2TJZTLLLCeTH($^DW;cUh%VBIxEbwTJ70ChDw|myw-OPPIF{L@DCD8X$2dDicynRO z_LHkjH5Ri*E&-~_d%>K2t%sCk36XU-Ic{`~R9ak_p~&}owWKEC*0D9zG3@gI4r@gJkEafy z689m!^s$Upps>ICt6307yh}H^ zqA!AoCP@8ZhubonAZ3{nrc~KG=axjuy%f->T_D`0*Zx&g?e|5HmW+*cmGv^(Lu@Nv z!VLjECvP&9w4D&S_6FC`6|_?Xk9*B%8D0TJR;HCAH?AKR61kF7Y`qB6>hR^uQESdV znowVFqDRJsm}uf0ciq$z!g(DX9W;Omg*k1T9I8Rrd~GS?TeAa9BZS(RjZC-4pXwP$ z-6RNx4s~AbX|et`c6JXT)<(wSJkd*yS)`_{AHjkwj+5!VFQ~pvWAL*{jWoUB`mwsi zQW8~Nh$dWndj3fo9B~OdZ8tYSg;5KPepuR{1PPIvk(XdaLGbX%fqSF&p zc4t_!QE0YO*HL@f&~IKw8?GhFqB_0(6-&aPemNKKXPv4sFxuYZJayf@HXKi)gN~KZ z7zQ-DTDxjzdhEBV_v@j&TXtQ&<;dF*wf!u!*Y)-cc$pj5?f+z#sFH{0suuYq9$9qM zF^qiYu!H`FkjbVd-a4)`9DaXkB)n7OcE|>pm+I(VdGcgT{>4fCqG62gL})3b3b#)h z7g+LRKLIx9pr&J}S0j?d?JDg_rwI?KH!JBIPy73uun!Ekc1tR-dsuVtsl3yBm}| z<6S%UZukojc5ip<-#R`DRz7DKM&Zx8D@rH+)`3zf85x-0u|NNjR2!l*JLq<;E@4#0 zkv7a792-U;IdCV|o(dmXLMcg-7b>91iC>9{VArpT&);5{^#8^p(;!NDsMi)*4d&h!W_4XC4oo*n@?TD zaDsp4<>5grqww}L4tsZ%OXli>$Tz`eAkRz#SGN>O?b9^L?P~moGrB|3VOTnpaSb8= zJ#7hR0qSsUu;lEjAEyj?kKLQXKe7s23{+>a(`&h9;u9OtI<(L6?kiV0=9h{)n0de6 zOgMWjKw|!?l$^(LC|+Upi~Es?z9II#!;ofIjU^hht=WDE(C5>9DZeISY&)MMFYToB#r#y=z)`!Hz<=w3iZf#cRbBwiCotxpu$Ct~ub58MMnbYFg zR!MS4*Zl$~+YOQFCNP)b#quvffJgFx(hA#-k4vs{nYp=>aEe`<$?=Lpp|$^glh?Ip zoLGjT5kcgKq?Y)>R8&RqDa;U1cRYSM;$`J;9c`M5`?$xkZvouOzN{R(Rc*S?np<`UCR6iOik4R*$`Wa3+rBGY8^*~dCr2F z7*4{x&ET`tE44cp+{?KnBW-g^p0csd4nU|F!os{v@PFGCZ4KcI3C9$sgD=yy%<6~02i0_&q`~@&_k?a||L0%f&SUK#?e4HxdB`Vq)CVO0|0|RDO z?AQ&$UD--A(=m)w?~F2%jp`=iT(eBnUMdDRsqbp{csD%|_inDr(gqw=TKd!j{zRraDKX1c3$NP7!iGT*1g+_%L&)&yqGdJ*;mPfqYUNukE#6(I^BIBrDEA59DGbHyTzqSG68w<=roqUi`>?`sGeA0J$K3d zX!DM5T4b+`rE|;(KIo%N-1$pl<+Xm#SX3X^5_fE58hN7uWgxw`3em?;ROb<5+3 zn4CIA9mq+VsMthjPVO*mR$vJ!0-(`jX0J|`ZAGj^-nwj=L=d`hHHK4n$wM8oO=DFX zELvp0@EhL`ic*xW^L0onJQ23qS`sF`_4NGAkKZ8(I!32gTnurA_};xpEUGOmB#U+1 zX%)@udE__m#jd1G+Y^z=+vc*;(qDNr(xzN_|flhfD>Wbrg@a z{gD1loqC655^N4)NZd}gmZa~LI{+__O@KG(DUt>xK2-U2U@wJ>8IaIvxLO0m)868Y zaU~Z_%Dyd>>?_C3?cOFwcn6_f-jTN-VMZE)j`MgUu<^}RI?lRFyG$<9#H|j?ct7QP ztbEt?E7SY`n(Qxp!dBub`G#Q+_rBxMEgKs4fKzJui8@_8hD)u+Yk*`#m1NwoWhCT` zw8qYBww9I9N&eZl^>tgwwwEBnmz5gmZq)`L>Wjoh$4w|&m-My$09$?ttinEC)G46u z{SI#c=lW9uHJS#Er|Q`+_S_Mcuq?4gpq?h486cH}uN$Za4VnefuEOE7wM|X00VmRi zJutf~`pouYUqt`(2wIe?Y2gb5B!<@8z)F9Driae14_8)3c6L8^7;(ScGMW{R{Y zQId84xsKeX`X2H~^3siHJ6bdSQ9IZEum|S0dXT7qV-R!Yma8)miVdfqAGYZGq3~%9 zT(aJPcMULUrdYjlTQ4L{0SI~W36EEu0}MZ>PT89$2avNLTRhXsXiS!NNtZ)u z*LBDYeDM)e)OMOJPd2V2Rx^ye?Y^uZW5ToVVKySvg5nE&B;{c`(FRI@8u6+L{xlZD zXi0?z4)I5x+&}$s@j}Cn>!h$pj%**orv_V5^eotq5FYbUTJVVUP{6)p_BQ4hoU;Es zBViuR$wo<1PEj)P$^RN9Llv~hi7S@9B}21q<3;Ov9wyA-G%{$i&5 z;qwPw@0I%Qysu}A)(s~uj^NQyb#bt^CMGc_ECSAdc8VU=X5FWhWk%E*=*zRG9asYd zO+gzvOJQ(NdEdT$5O8Eu&{DHEAEgCwVVi#mbvI2Av7@$~BDBZ}>EhvL}3wac8R^IPV1Q+&+N5s$llz%j$ zP23+@zE%0VZsqp(di19y7E!|nr+a`}(703~13cYEH=XC^s&?*y2HKwPV2>2Z39$kv zb$+l-%s=c_?OCNrDbO9Frmw^M?xwk?Py?U=+UO1F1>Np3AD|Gk2VEW7)FP_31ak31*4ccuCzzvrObFc$wqG-iLPZppnHoAKDCs9_Gr}Q~Ziv3|Rig zi1Q|Y{2qKXUv=ztQmp!mG!gm;Gt~iGq+#dfK@Rk93a~;(#w}I979Iv^eJ z-Bv4%k?f`^VG{+x0Z?I7MXJ@aqtZ~q7phZv$UvmH6qs!$4l>1B%2dfNV_kERuZe?{ zY*cwF?gUT)Xqmj8>6FP`yPqFl(nlW<+ot(#NKIfIJ)nwOWFO$M8=W&@SM&Z*(#oeh5}pFX0*6?^l=)I3ksd& zirIDs7H_e0P_{H+r;WEYXV7qbmsob$Z5ehtq|Jj0KwErr=5;EInbdY$(i)08Lr45F zFBI2q|a5 zXH0uvgy3&9Le7l=JKPzPnR(pX6v?GCu&rhN1~FXzJncWqe0pWy>iIkjCOCzjZDFLS zVz|W{sGFw_3@b&w3F|sF$#Li~EBWLvziy;i;mTKh!ClPUF|)cO!|4Uyl1f#N?s75f zsHZLx5 zj)788^q`p>INbWKYGetdVa&9zv)lf3DnXkbxBuFf#(7;A^=?BR_n~?CAbT%P-z;3H zn(%$K{2felNDylzv2hxKYuLeqq;cV76#0rn)>>GciHdf*%F$Ts?QGast=M_i%-(=` za(1_nNoU4_;?9Z4hTRW^cO4^n{tO5roM3l~P-&k9!**wQdP+p`Tob-7V*kqjs<5!d|g}PQnc;f0dx}lT zjpC3ko%b|HE=hS@eE{f5F9-DT7C9F}OWy3q z{O#k8%E$_nNvTEMR;!&na}AF_nA9}5Qiw>l@wrD`Jeq{!fG_+lul%o%pOzE%xGUdN zeMD%2e(&~X4o>myDh7I_pN7+O+7Cd|?E=gdBQ)y~hCIoA`~F=&@! z;&cG6{&39Pv*KLyC0J*|hF73(wuXTL)`D-BCAR@Y1z!}d7$byX{@G?CRL^ z3Vs&o@1QNlOEV|j8)oHfHz0gjlDYMct zzoBzxkmIsc;b-&F-UyT3<=#qiTfiqpMF}gyrq4~wcpkak`_3*0Hi6=b&U(+-oY=h6 z{*z}oqLB}hKLyG6^~uhTFj_MH(@MOsm_%fN*lrv~*`^PL&;dpy8h`?ro-XawunJ%P zNYY+zJsmDPG?jXN+cm?1%;Sc+4DcEvHxYKaf-N$?m^13)U3R*^*+RArmJfRqV8~SS zd7J#3A9GJmT_(s;ncO6DB892b+qlYFe+Yh zkmQ@2%y6npnS}`&JQNFV6<_Y_e1w>$-k(EmB*oxa@yWa)of_-<$5;MJ<(}bY zE0iRY(dCgF|2o=JPAjfIxtS5YUf#z{zW<6msVvLkXmW5 zZ8g?2w=Vsd5D#jK%H0;BK;1Jag;mZ2$DKV%U&@Pwu3#C1^*yW*y~S8T&4W9YkBf_7 z$Q+!VAqOCI&TvGvV`_7@G@9R-wP?_nWQPEX@ZLc9EOiqTtYtfuC)AA76n1)biPf@w ztPlTAn1upfA{!qVS%Rx6mb_#qg?0MLMARS4?qOuFWb^s)#+9`y>QA>p(#)FB)eN$H zeOFP_m!E2B9eT6ocqjkZkx^&bB?!@AgEHF8cUWRqWcJ0ft?@OOVStPr0&-2?FX51w zI6X1e;~=tviWM)jT=p=3qpEIOzupn6kpL%Wq(0ew{&bu&$dW<31tXa-AIGs1Fd~s> z?G4cEI~CTQFUI?0v~`m)R842L-Rr^OOp#t0mQBsSrVkzZt}JrNZu?NAsJ$)Uf^^85 zY>bSJkFW>C-@J1(nj3;Z2+D1MAo0mL5!n{k6y58g6~_#K4)1ouP00k6oE7*ox6H)w z{W>ZroiwiNz#3!(U~+LC-XjLMzFZ!M{03wI*P8*I%sbxJ9i=fIQg)mH;*T%>x^EU> z7vGSm`W4j3GsqnW)0638Ejs7px56j2NrjIzKlRzgzShRWwR>-o@WM{*53R+4Ww*)x z>2@p41+tuYs86<*{EO;%z8sUB6IMXO-Uz!xt|(y~4)kHQL`b>$O>5}Bjz6@&6XT@A zqJQh1)uG6UC^Zsds(7h>{6#Sy3Q%|;qhwB_i{3!SQYMavxk0{sYFAiyOacRQmPkp` z$_4BMv6Jk3@x)oD3dR*;kQV79?Re- znC=+9C)%VTXf%kv!8g)SqGhKU>|?q|ReCN=EYRVCCsoBs-{j8TM4#?vlc+ zvM0}^4qVf6{H)QjIa!hK+f8&nPMG{N5Z&RiL>m`HHEc|er7>X((Xons)UD^SLp?C% zOgW60glBAOiqyXVNNor54{lp9ndn{UTnH0QVkBKyD9Gj5%SjjL`JU_2>gM0+!?9mH zR6rs8{T__IKLi07;!kK#YHWjocH8l8&zS9e9-_>W#+{R;Kl&{618!u*%Yk)lGTQI@ zh`N`=-tGTbI`2TN-~El-dv8Ma$_`0nWRJ3wA~Pe2hEVn%*+R-DJCbPHn~Ws1j7p@U zQY!Sjp6~gc|4!#rdY;eczTe}zUe|hr#PV;x9LmrAx*v#MSPco3CBCGX%u`}0DgKe` zkxKQ)A%VU*bDN$CDj3cw=@5lwr$fyCJKOQL1OD(UWGte*4!a;YFqv%x8GP>M)$ zdnitKAhYW7#Yy_{vhSrK5_i&J^gt_x20--Y^hnn{Ax@k|nRS9F5A%2!g;5eyxa@VY zr-;(R9EyN}&K!7$A@s3QAO56{J{7t~-lSIT06;ZZ9>v^lJJTR=5q&dqrOl5C8}pXq zCA`)=|9<1jYWb-CzrrfU-KWodJDN&=sQV53Kk3GR*4pmWgH+1LSiEd3zi^%ppXh0K zkJcg7W+70=x5tqv(wEd?mqfq==PE4vIJ#i@+j_2ddn8%k)zR?{V6=1l6nn>7oIvsd zsELuP96{-kF@w=AdNt1ea?1NSm}=sh~?S^Pkl^)fq#KfDS(B|WmH!mbsEElNhWI-s(sgGpyjipy>m>pO{YxV#p!H20~j~i3eI$YxBDp$%1_D zb|fcFEKO}eH#y0XIX2O#ny7}3gPntKMRHAag|NB3c(GZ(^f!bFCuDPaTtKyr)sA{l zioxJ{Xf66F!0ds{?5%$vABG>Xk&zl~bQEy5Uc&HsUP``dbUC+M|C!%wEW5GLrBcwg zwxnh$9D}YNkE3upTu(YWyMzU3^#tRI4*NWIJaS?z%kuP>WfTkzW42GD+WLbzPICxt z*OT%d-Q12zWA?j*-!<+jEi21F5!HQ{OeQ@)|E1kykAIPP{nD|sC3%@_Pe+g}LlTP; z?g>;)YqITZAAkMr$fSEbDY1_!>Hc$1CG~q8amrlnB%(#-MC(*Np`#_Y0NTk`6Q zPur|o)ZQlTN`XXPMxA$KW%|B9XO<)SMxa6wAl+|#+%8FT450$ME@5NX|69ysm56Xo zBsXFnN@(**D9wBcUoN7Efx}^btsZ>dYBueu)(vjt1IGl7>B|2tipBQ}rH|&R;arp& zIDs)2Yd}Px?QpNhRSApaixP%gt2|f55UPM@5Cs-Ckagpp(Ft_1c60pH30L=qlgxT5 zC6>g$>2bR~G?@GQj%Y!DL|fNez9SaZ#QIvq(HadZPWQ1NDNVk=Jm907QkFCE^-fEI z8=KAP?;W$|5^PjtW2YG?1;)4Gff8&d?v+z4yC!|?+%vO%A#i(|ZRJvXxfCnOW%_Yb z<*oDc>GYdcXA|-AgdsICwC$}+BX})QYsc91z(%^KRU+|o`PmZC6i3X=${Ai_DtY?_ zBvgcHFO~wq_Okm_yR7~9pNH2&~mS7YRW+UH#0rHcvv9YOGCSo0F z5H|F2;}3xdfh*nn7;CQLYVl#$-j=&uj@RCfPlpw5mH%Yl|g1FKsUkuE*K zl0q-!itaT%4{`Wb(|C(Whnpep*Xm3Z<2$EM8-HM)(yT{9rB{~anxVk94UZX|5 zCM5Du6S>cGoOeX|SCgFbx(>u~jDLCIC0N?bnmPO!HCnlj-GCO%Owbq$=5Wl9B zV^br4vU@Qy@>J7(h|d8XqoE*R=v4CSuuq2@7DMOv=LB1#ES*)4qLg-1M7NY{DhB^# zvptx8&{Pa-M}9;Uq+1=#S6nIMwia1`4so!tp#cz7R?ePvxN#ujmT>^7>+siK2W~#a@+`cT z)|)BfI{`OsAa;n=J+Qql{zR$ML`3&ivPaH%6E+p}tTdWEe@H#w_U!$r;?$Dyd%|#^ zpxG^JDHO)e&huCW42yiFT>{PpyV)bE?eu8`e$Z^Q2@_e)5P5H)k^@q9AS0eBGsAghELRUO0g>|XN)X5DNZs3P|-2=+FiV;TsWb7j=<|$z94j9 zB1_=mc^Us4=M%U2JLUX>hwO5HI5yBT--}oAFSq>Hc?4SqTwEL;Io1fb7%2-Hm*2r9 ze%I5&iHCRn<+pA<3reFraErC%-X)7&{L~E`D~d6W>3$yu$PBLS zdZMv#d;k9ZgmfmEmRGQeMcPFPQ2DkTXTxyy87-{7_;ZIL5F~2|H7wgxl`RAkX52?7b@T-7Ck8rFzPfe)? z(k(^V6JOoNn?Nj(2vd;#|qrHN|~ zUXZb@c|JYo8Pj`Hr<+vT_=K;MzOF8^MFA}=zk8QW+fGW3SKcYyzU~P;F8PkxH1wdmr7T1g}QD>y^14GEs3YnnW~q#S7Kv^sHT5MN!AYk zGfLxfT&vhDuBbq|Jw>IEwl|;Y>kz~Qwr@_mrb30Ty&_FMdUp~K_f*ReT{z#g=6|$q9Ks}OoZv3u1b=8tx_QrAGr4;jOZBH8;C;q3| zvc93^_fbD*nx?!-Q#|08r;>N09iXlAeySghn2$Bs*Ohd;gz#s~tTo4*$HVs>OWki4 zX||7zW&`&D<}QtP($){DHq|;MMz-y!_qRl{fhY8-usV)lM7c_#zmQmgtk|NQM zA)rNuSg)rN$wkIb^eSrK1_1ZLz3w*LVQIijxp{4LBPw$q2%!cw0#*ITM+==x!ji~$ zM>gw0@XFLE(p=6++<5kBqq{8Rr{%86_a{cH1u&>fpcU=dqQP4<^`T{u|1K&_+&TJbm=tF-t)4HUe(5f>%sOUI|N>6rGssI!u0 zE^!y(sko9AdWmYBlLe=kXkrL-#MV6$H_tTivA;M}zi7B(0(dm@C!g*mcF_PXrad~p zNc`AnVj#OTI=e-kXzjWYaHdsnC$Q6bUDPX5cN6bNW3+Qv$v0+FlMCzw;Toz3;<}Qn zpa)>J3ytNR3?XIe#@Pu!14P|6;|d^poql?d>p0$a1f1B{>FbbEKKsD4OYcKl?U~|l zZ3{I~Ojm?_&g*8Vu~nKUZGHLv?Twt)$=Yjyzw;gw$=-wKZz13zd4dCxuQL!b4h!nh zir=;hr!D4)7Tc8ec%~5_=<6m76@bnpPLyk?=$N7=BY_C_G32k;|IV|Vv;78(JP0V4 z{r~Ac@@-d#G62j>kpQwPDZ9DWfPO`aQw4uS)|I`8JR?F6)jSvCh^?gIsGae#mC8mv z(}pggbG`wtpFGoX&$|#0;2rd9rT9ddG0RIG;X2;^dZ7#6p!5jmWbGUl^3!}~!k#@A z59rE5Mz)`_e(Sq7tM1Idpu2~8K+8{~{RBmrh^;^7-vs0xvmVl6?#%8pep1BOK!rJe z@T8M`E6O(%-7ZTp;=)uq@ECw3@dcDgsJ}In3B+m4k>n&Lwdt78rYTSuXF=)b2iH#g zqkXLk#XdXzKRi|^g{~dsQulOt>iwd`Luw2G)MKAEA}_IuikP>l-ISG)p}!^~@NneU zIUWPDdONv*e+&Oib~n(n{@tf=KU_b~(s+~8fkGi%%LT3j_-hF6OkD?R#{C)^+t*Lm zV_gx!Vp#hu8oo9^N-Wk}PQQ@dyEl{9p`ig=GHh-vem(7dV3D^wMlWgV=2oGn>p_D> zJ!VDj$+C_}v2rI`bitNY)^4VNzCuZ-EAgrFm8T;n1lp&PiT^})Iz4bMIJ28yajg*c z`QI!ojKIcabolD@l^oIX@&8=x1l|*N;jKJwYnp0sYqoV+m$#J+KE} z`ZPsPU${WSeO-$Vo3-kFUTg^?Rbq-YVeWzfMv1C>y;w;6Iz%o|@LWhDP#U$}V@PwI`_zW}g2ZPnTf8v+c9LLEgbh`Z%4K8ZT+`6KxG<`un}uhk|`-U@X1Qg7+U4e<6n z_;lo{;U)_wNjSCgOU0Yx%~U_)&m3f<0aJQTAwjPe7daf4cRD|OH}2o1txHtp)2&NI zE@NCid*uQD14Sk(f?ol3G#X)^_&abc5532W1ii?o=QTL{@rp9DPHbi%p4`r^zl{tXj}g$l^|*Yqyw7>Y`$T?Ve+0@0xPfW?OEtP^@Uef2>K`t zN%ycN;!Cx9zOX;WDno@xIaq+H;pJ!W)E)zqb>2&fNBRc#AsCeixI65UI+9ks8u(#J z&5-h6tUO>WwQa^D7_ z@^T=d!PbleCXU4a{t0Xs=el*S-6lR-`>&d;;-*6Em-jqZV_(T|SouBWc{iuM2fpsy zGZI7yuyRx6zS@gFNrH_={^~z!YlGLROsCUb!sa8!HgDV4^ zpiI;5h>}01#KWf^=m&cSOYTzijiqz<7_tG>o^9S!(lot95)p1(P5|U$g24k`;X_L@ zO)(mmC)u)vK0dW3-*@X)QM6jr6H;U4ll}{#bnA?adVqAJACp%%b+1aejL4cdxOUFV z8-{rPeYeQguF8Dif(T2{k8uNne)QMhsuRx^*DI~7akPnTlK#trI;*p3M3Rjre*~*y z+d?MBY6>QmmFY&|54(7xv04M^41EYd*Cdymmnhh2#!G%6LNk2Hs?kS9ioY8bQI})LoJ+?m z_#aZ#n&j{H4v0CB?x=t37shq94Ra2M>lc- zl?xhNbS`hdPSx$&@H9l`He8LeCkPPD1N1hA=E*!)KY;E0aiW}7;PMqxM<)7$<>rOz zc(orCHdUbV{KtynnbLib;t}UcaoPCer_6t&<8TPR1;XJxot4QZdHo8J2-siYaC6eLH z!nym99^)l4XqB^I11IZQ%~81`(On!nJ|y!FC)0%$beu?}4YCCLN{J=I?S=2QVw?u< z%nxw8P&9{^6W@=tta^n?1mhEaNX0&cjzud~6j^ZJ(Pvwi#9n8Ulwq+ZJMeLYSB13{ z#!K|^WEHHQjZl#X{Q2SKw4$3(lBy6Zr7MycVcxC-D+^u_AhU^yAMSlF)4-w*z1!lk zwor{DtCWF`_V)I8z|xxQ7uD@9R8Act9}L2fXrZmpmVY}`!ymPf!>Mo3}xtKh6;sA&@mc@=-LC!r9k@k~ljs7+yPw2rodlq)+v)5}bp zEVb(G*FrdHv2E5%A=@V@33k-sJjM5ra6Lr(ntxfIB_r)^dT+!e6rUzF&q*4hLxwz+ zAmjLf0l!b}E>XFpYTZCnMc3qM$@xpq1Ne(Ms+Kg;7K(UY*kwJHhSp0*tK)R8x|@Bi615ymy;}Vwo2GYJqKJH z1K7A{C1Dg!Do`Q4Y4vwKuj#47V+Ca9-lQI#GBdkJ_)RGd3Sb*R{~ zsyBr%=}s7t@h4Sw9_#Q_UC&htJ(thwj!W6yA1Ya!2RG7Zc{3h_>75p2qXF&v*0be+ zhd7%YY-}!~G(!18ATX6bIAOtt+aqb_uvy5b*4_MMNlc*L-_W?MLf%%H+XPt_b`joR z1h+`zU;CbHI(7B6?=1@J@WjpS9mqiN`Vmwq@y$51+TJp1$}>?3n9a)D{+`tl_a|T5 z_v0+-E&D}TN2gEyTTm+ztP}sU&w;+vgPBf=UIXH1w$Eoix>k|-r`GdIk;*qPkJ5P5 ze=?me^p3%2_jdBlDn1|9XByXWZr7qRKFsuT2!^hf5VT< z7tA025B3{OH=%T`J8?hbx?jm%#tM(^=e%cUm}ZaJMwNNcYX;?YISj4yDl;di+>wQR1_Jh7#_u3M9sW~F!xpu#j zY8VSYWOY`gWN$}waWzX)k0N}XxWHLv1U%0@cKk|6Oi1Ak`cj}FXeNiP^`=0gP|+py znzi-y6^=!4z*fl9vE>>&UF|jQAU=A+pZq>zT_8bI?8R9_$wi(yIPAxQiB1Fk7g4$k zD_#B-Dcal3G)hD|(caAjuuMD$wviJ~kfRNbjv8#yDhFO!iyw{N+lvI74kxW<5#Lf~ zQaAN%iGK|v3Ra7J%R)?=#y z(rZ0xkM~14P%K`*4IZ-vo_+qnxrkSgKEu`b10+N<6f z8SzgIc}AST-;J8_IR$S7^XA#HIy_0x7{NM+IMHgXR7U#my|7E9IQ)8MuLE$rab5m+ zgP@Tqs~$_eivbUIFT$GUOk_1uCDf zK);1mJqAqdntdmrwL*`K-;8?G{k%q~4vzyy>#$l$Bw`Hs_17LYgeaUxfU0#5t#T5d zyiNFO`#v@wD3C@V)}?6Sex1tY3ibZyfWo+V>TF~3hL2dkWgD04mkL&USg_$3O8Evj zUef7HLK5MJ`L8_P8xeWfP$R~ApJs<{N?yhwtJCh&aOHO~)5J*a*+ZNK5d6)9F`5hRw+>uicU(0Use+qS_wyCkGWcHQkTAkm{ zhzmGOdS+|bV~M`Hd_3Ktaf&BIU>BW&6;Y?f`cRn$=lii-^@dkz%V=bwL%jl?4le_x zr}D>PcoVqy`M$#4ntxduN5LQCQto)jklk_6q(3W_g4}m`JrK=koC7@{Gp4PZWpUC< z?3Yiy8LvJrsdVl9@%**?O9D(VFDO#B62H(Hyns3t+B1n1z1S;Q)?qOr_K^74kM9O< zf3i|HGd}55bJ%&V%np7!IjByg_<183WOjlG5^Wn)Z~t}|v!eQf76>f|-3tN+jy0wu z%#*N(fyR3+k(d!|RtQ%G!-?(~N$_|i7x$Cok$H0IRv^I_kadF;-h?NZi=dDrtdr>@ z-211e?+?nYoIevN$nPf41@DiAjg8*tZhwcHhhG5iU?*5?9v_$d?U#`oCDnA7Ec0m( zA;bQe{`+sNp6;7JN;9HHc8iTh4>Hp$vv|D&dk_Un_=NBa0TQ*Zz!NwKSAA&PN#G8M zgdL+|+wVLkT{>c7Li$d-=DAB$;D6AeCo3xDJV8y!2ha_ z1RcEYqzqB3j}gd~`5i0iH9%=Pu6e8ECzz)_Z5LnK#iW(IOJeEf@yJH`-YnyCjHvQQ z-1gnM{)lmF|7fpZnFq5T^EC?g!SSogzJJRg^{e_1~Y1 zklnRCz~$Pogfjk?Sw8lm3Xc?}zK)KL?#8YRoY(EKLyXG5xI{z*H?n(AtYIxsW;W(d z^-8f9-#->}w5x9aEkTsbhDOfIJo@xzF0<%rH%@EweYAX8g@uGH74rn-wrpOtZVo)y z|MlRVes*ug-iA*Y!iRQKQR+oFd(N+}t^y0vOt`}Xk?OUet}7oG3_OiIb8+p^L$ z>j~dr`(A5r2=1|6#R$f%bQ~#9Q61rFdp{NdWF2^t+JPjSg(fChe-ZB2Tc&x4E6Ks& z2uxRB9tD@~pwlBDJYd8!pFTkBlzb8YSWMI>gy%kX9&vM2i4joX;-KYgE15IleDzjY(41S2*#agGj|q;HG)Clvw)z#-mPb!-?oKe^jNtV zQy8W0D8R%86(1B*Lg`f2HP4oP6#Ez-@cg7(ITpSh@oUeg-0AoibuzX+VMk2njx^K< zCD04W`%)6`7~XoeUU$oL5Anl?9Zu~k(K4IR;t~|Im=Ua>uZ+*iVgs`kABTu>80)vV z%-#!?2YDi&hSFu6KQA&S7%g|E>65W{=g8MzzWv$*HFClN;14 zLs?>rca1bgnP|Tz=$9f37PYlr1nap)EEoaYH8>=BfyF?np8N3=yQ3%Lm-+g_n98o7 z`NLA|`J|dvvu|JcNU0Yx2?^K|u(~c`Tzuf>VN)*oUoia>Xj`E8L1bgolzd3%sWniX z0gw=4v^K3NQ)%OP%5Qpx;Hv*%^l`gq%#y* zR*h*ZqHBcKJ&FY{{aDHS4?_JF+&rqTqY=T&PQ92c>{wr4;eo?-jKYiSolt)kMT2 zxOX0d(>A>4E=xBf-ivRLnX4>*gxrfhvp01M%D+8eMrRj-1hgx6bm=b;B|zOJYy`X! z^LT3Lw)L%6dv?%G5%`fKmA%Yl9bbRJR*mEdmOtc0o*O4K?yKhl@T&_f=sDw#x9iNAc zwDbj4UZ|FTAMc%p?l~hpow!0&w?e0wsWk@h<+pX$>uFE%!BecJhI0m=2sY1GjfQqR zTirao4@36vb!;EEm~lN}4qh0xlgCAzpxw2&PH-0zWRy}W^P}y1D1Mz|^-Bupgf(T% z8PiUrus6+^zMdVGNcYnhvPuqPAnVJc`Q2i;g@l+3f64`gq=_621en^p+Z7Nl@?kg+ zqa}?1+3uT-Ckat!)7`k1HE$?A5&i?K+Rslf7_)t*4T{}v9$rErrYUmXjmb08L78a# zTt&m(-n!)sQ4hDJs?j_kY_UsZPV4%qiNL!XH+YjNHqx|s>zPklb(P)IzNUBPHL6sqi5pZ#1e>;8$2+9yr4Gz`T{l&gfDJ9mUV9fI%CjL5UDf$YB4>nDI5F*1sE-k zQE4;HZJe38^PVt8pqO4<2b3^I$N9hES-(cdHi}Ts5 zr@5X7+{2W$#EFF=c614Vlr!h>=f?_-<4I@94+!rT5pcW*FXsEF!pn> zu_sPkRO1VN5rV)yJOHF|`*rSu!G)$1!@G7V{j)YjIn4C?Q2%|+DPJ5L99sG=DlUFm zy#S{_+~!pM9(MD7QP529VD2Fn{I7Qq7w?r(jbB%AEaCkmm(Nz97-W|ih~p3TgmqLa zlX%?tb~~*7hWOgnB3C?*+q6u^F0$%bR9OrCI^Aez{&nQ^{aAMX7lI{|sn>IFIAxWD zMklilx+$$cTr{Y)FqT>H^)p0-6E*F<0@8mibN5_ZB$B3Hkw>xR&u0U*%tKQ#d zZo1M2tPAd~>!)9#4{u68pOFCb2&%J)Gl?-|4p)Gh1F;;C?v=ei``EpQcbzUPNaw}9 zfipi@Vf8o+4|$62G`;lCQw4gT3L^3RITm_NF}e@E!0kII_ILV9E5{|1%w_D;vNks>GbB!HHk*wo%V z(wgYTTJ$alo_$WiJBDYtorsvit?0=}ih_H);o8swh~Z%XI{4fhV*~LvxRiXLibz(l zo5m$o#9t=ltTdG9nU-3@6od&NR;5mE!Zz_rcRHLt;vqkuIuj%XIWyq80K`cAhwiNzeL!IF4rm~*o8bE{l-R_gupgos@XZK8iANrgmHv z_czj{MhZ2-1wo$WW zouAw#Xr;0u6Vso6oZ_H-uUBjyzKtO_=8dlqRWZrLxI>getaW#wpnj*XZ`+3No@(P} z=P8}H`!@#w($sL`a_uIEqz{3w#%*sk0mPc+b1sbOn#c7Di}&r3k&zI98J{lCiIjDG zgZ|@1NIPag2LOacB1`=Xe7;i?_ytJ1FaP*RtSVZk+{>j}l5H&7P7-c}dALwtq=W3I zwT|QJ-KsPPk~w(7!u3nFxA!k5i>R?zxYX`2m#65e`w-u&vaQj8!R?OH+PuFBmifAR z9963PYm8+|8@te%PTq<1O*m^Es((&p z1aWB$ROg?&m22%t{4!Z7Vf9_(y6?_kwW$MH;-|>dWe$N&j_5z+kt|%w^@)zoafp9@ zSAnO(+3k3E|9VFEPPV_gpW@iq{hwD)lYZ+?(LWQ0eCs%_J$V&sH&a@Vu6eTg_lAlX zkF?}<5A#rwpQ`=&j=SC!Q}El-UT+Ik&eH9K0rU*uXEEm`IO)-`P_=Ycn7GO$4Bj5_1V#)YNxHhaH=X;f!oz|3r zEi5TBW#qAsG;v+)*z_Pg%3LMNvZ7mp;qUa)_nXV?#L-dNTElx9UizuLYc6`o!{ko7 zP3)!Fu8WHY&z?T}r7?aG7BF9Kz8OcSC^nO17_#rzE|dO3D<=-zGl1SHI!_h5h_WO| z<8)>=CEKGgnl1+z;ricg_*B1xyX2?h?HumF?|yK4u6_HIVqO|Mo1Q;9e$?T={%%jx z_Q#(^HGt1?v@3Y$R1TgR6y(?=IqfZ16K;E3h@@-OF9<;R4`US7k(%%1c|wpy*{zs5 zaF`3WN6^FI_RI~?mQvOHS5B?i<$v?#pOZNYP18J1*E=_!Bex+cBCdNV`nrxFbIcAc z=HECcVHN%b4QZtoqIDB2X=D&b09byYl+kn@a zS05BV;#jvoS z%(Ez-H6A&oBdFkzGpaXP7ZzAcbC@K>Hh(1cxRBKC8#itsRgjnW*NrVb3mR>sWqs}~ z&8sd(hNQYGNktKGLqks#s7|!vs)qpwygeIe$xTaSx)sa310>ua;6V)o2}U#4E8U-n z`zbP+jvESD_zr#!*xL)y!Mk(!iN5{{e%#86VNhFG*ul#;(okL;zx9lLCv5fcr7LVU ze-B={_jUNE8A?W(GK9&5Y%;Lm zw$-ht+rJ32JZuMgT#9X1JpUc9eaj+uP-}iDMPQ{&bf@g_t)c#Y=s;Wql~@i`dZyi= zl{w{B_E(EVIHNkEvVdE!O_$Sn(1(?2Smz2y=iIy>0X$1+(J*Kl=q}G#2G)`gK;(H= z?6br`nty`CA>~s*-arG6gG!;-sj(yeNAP3#{vprS#MVXiFz(>b0Z|WxH^SLSKb|p} zDbA3&ZeOP&CwGKE^y7FG)txoTdDPZ$);OIELQx~Btsl1>Z;uWQMa}KtyFl3*G`8IH zJk_`5FY0j#N=XxL_c+|4`@AQqUdsIR`j~}&nUEF*p3Fy5I(LS^M?%lU@+fC{8>y!(7_ zjGRJE0rUolHk~oFPA1t=EeQz`Z*|7VtomIV!9DPlOE_(o6#DMlv4xDO<5g2O3cn8U zDXTrtwh;*yitnkP@E>wMz)YK}aH`P`)z7#m`r}1>d}xuVw=G^7=tkr#d7Z=CgzRXf zv(iX33LesLG>^4@fOW27FQSgvQu}~_DS7c}+<-W#bad%5{?9N?_%_XuX}3R(JVMNLOIy|Mk8Xg!Oz^a<%bM+95SHd+>}XB>n)UzgdoM+$^9GV0vips{?-cz~=#Isl*{6+Ub3Q?d4^}p&Qst8`#7jjs;XLGag)jHFrF^SSaoXWq z1e_a|ETEM=yX$4O`Jm{eW2>JwVzsX72ll9@osPKxX$sSREv`u?iYc3I35T`KmeG7Mm3IgDu zfff2it1Qh{G4bcDVxC_batmR+3br%!QV|v|#h6$s+n6JafXby6_!u|p1luSkUYITY5#gIK$+}qC?XK_E0$5Ob4{TP$~0 zu^lDH7?h>T$zv*Qh}H{D=15(gb!=H9M{_1Zi_S#^^LH<J;yd7EG#T5#Ny5-5tWRp&)4M~nN#Sj_(h87aF9z=JjFBiEZG_GnqaZ`RUxt3ON zjH%b0iJgw3<;=^U(3gImX)<7nX1TCC#zs&!0!1HFiZF-aZS+i6d6Z=bHw7*?w*R2?QBPKyCdTl z>@EoDg^(}I9gW(6c%P1<3me+9&mrdHPV!v1eOd!P+b6S^c(wAS8uW`=HdPy=+!R+VpclZ+u%0O+~Mm(Y#xz;5V-JKdjbrPBpV6>v_DUB!^M4VZCM5 zJJpn>pbP1yE51EDGGQ_zWIPt;VDJKpPbfFAriRZ|w}?y|i!v;MmcG9RH5}(y7);~V z`}AUyEe-OLs-zts`Gyxp;pDWY7O3q&2VjVoLSNPfB)rzQPcqcJVwyQY?pCW9bZjeO_ z3Z#1S_=ZGWw5sBs6PY=#+vnU-I=2N}4^GEL4C1xdD{bp$;RW+j$)|B)hkIdjp$?k$HYtB=zaQ_H0_->|DZ@00>$A7@5p&@iSqL=e;DRg zt*_jPQR34{OB?90HGGwb67y}q%QI_U9luL#+tqG9^-*yhLJ{*4^t;3dq+3J&CscWT zfjh@filLIwM!Pi|AxHqTHnl68@)WG|q^%kdE1(`BXc-bK1@jqM=&u<7B_7hT(EBYh zG!>o2l?L1H;RtiJ?dPwGF%Rr zZ|&ux!>ngiKESMC3F|dNva&FZO@jBy0b;Sb-x=JcZcJJsh%>faV!t-gQ17j^I^9He zoIO2s($SJXSJX}oy~hsqHwxU8nO@)fz zNxmV>Bi3lHl!Qlxg=3qe@~-`6P+|)U)iF``*q*REVIIi?_mX>D;4kArDGYky$!;sd zJaUf9SUuw008&E)RfOUo!~4m@q^~AHw~OE)_oZVA29XsNJdz?Fet&uZDIPk-R1{It z9X*y?4~2d8`bkOC`nrvYIW91NziX7fKhV>&A+|YTDd^`{Zx2C1r4gEy6G!h0)ITp1 zAYs2dXXfb=VHT&_V%J6bye&e@39gIe#+fD$;cbR4+Q>bG7YIf12ZW7nydP{;O{?d0 z#mE5t#9tJ8d<>MV+($gc-u?QE<1qxIOJMl!0b$|hdd*Hjo}S4@aKq$UrABE$#hmjU zUF}7j(Ab={o_VQ~+IOgfQ=}D+EBskjGf^>`tA>|~nlGyIJ{=l@#9M&htzc<8G)$V~ zGr&FEl>OtiWf_HAEtM;2il~_dRTbLav2}3rkv=zAD>kc7&3-a=Z1T zRPNAbX9?+NO9;IvU{WeB-1$CB-2AyijpF5zTTdUppdO>_YV_&KgGA>#x%3&{Jg z+<#l17!LxV2pbJyjEjC#Pr^)E$Xzk-3&g9a!UfMVeq-YjW!vhZRq)qfU-qe<{I$%t za^&#w{f=`7zp2B&kHysoM)vA7Vyn$$8Ffynfcfr%H|3>Tb==gCU8M3VGKP1z!vl%o&N-5HgA~64G zcO6%q89}WHVNqe|A1|oHP*}o@bh1>RzLo{YMaEE(XX0m>;8V_b4q-T0pn+o=?=aKP z&{~rQahJy27oV`1KaDIT>sPj2Y2PM3ZOjo=@Nd_+aT|^6Dqgu$z4Ejlv-?Nmh^k>e?74d+cp4;i++Bw23vGIM2AEk^fSguegSb=8V z@>1|gl;Y+8_f}k2P7>mrZqN#?KiRfJ%BU{z>qd@6j75FsU(`CT$q=@C;Amw_)y+F8 z!ZPzHs7b$+8d1dkHJ_2>+_nxGlNC#ehq6!Y_Z-C_I3|(BC z>wr}U;9NSz>dO3*FMNTOkYPcw^A%mCmVg$Np*L&OG7bFrYHm2*)&v36# zj7Jb{4bJgmUQQ_n>?CnDX_*nrd6?#_e1=CII%@-AEgt>uNu)9;Vxu5tcD;yQ{tJmI zbIPLMr3@((6Z!X+$_FNvGng`y7FGXa!8JU|US_fxj;ox!ou0AuqezgFNQbFxc3pvG zS67^~uy!sWKc)9}|Lup^m>>*-C*9>&L{~Cb&EZ>7zxLGJ9;%#86L{dNlK-;#G$C0I zLwY41Sebfm0Ca{rEf&o3?w07iUIeo$QXCsRdaPEsz`uK)tQ zM-RF`r8M4jocJHNPrH+Re2SR_UGF(&(CH?tgsCE1r-a#2CY2p@##tiUeVN<#wV$e~ zy{)cw#i*6@Emd2%Zg8nQ_r|1IcT?!SN1EIH8 zYTWY&%cV~wq<5Fun5F*OA0d_C9~N0YyM8Jm*r~NS>!L@pvr9WI6rGT%-R^LLF#OTgrt?#cMo6CURbqbr0TNDEbrBy*u*_r`+owCw~lx z*!v*OJF-LIPYc957lL0bUw}U6(*~LhU!2pGq2la^U^0SnZf~#b_sM4%#Sv~`aD=a6 z_H#DD?JT^fX91!i`%pPZn#47w#TFHx`CIzxFw1;NO=50nk1mUhwM(^SRkFTt?&w1A za_zTWG7~z2EMvW@haA&$&DivGq`4Ht1jpxy5Ky7&k}Tu+XcKE9-OHM~5=HHai*|KW zC|+bvw3@FZT@x%-7vOH&y>S-0HEmas>RCAEa9=>6LI_V;X8{e7&4Sf=vBJK$eCNF7 zNq;ZLA-(Sorrj|V`3x2893cRm`H3~>CFqx;mP4I|rI$*@hL{1$CWOXzd-jwphL}|NFY0V`Q+WK)0ZV0IlFe%L}5GM zO|YT*i^H>z^Db2toTJPpRfF1hID@$tdb#KQ0--=6V9QacGUt_dv%H$KleUEn?!cGU zF7IZsn_Vyv9>g_VWuOzAJQ3%{AiolUYnkAg#GasmqZS+|JQ@Xr=p`Wb$Hq7{S1eQQ z6w|tj58mw|HyY>IX_UG*c$U&D*anGd>J)uFz7e zk7^>k%y>wU2R*o>l7tWMP=cz6eFWP`5^!~j? zHCeP};)m`<|E!4k1A^uJ1~Bp*+EJu^pjcx0O3`z`vQCblIAsycp5XDS=iZIxBXI`plg0Vvhft1I;yG> z*oMKm&I!{=@eFpJZ?fD{6_R%P0@eF!n*xXT12@?n<1`CO5inxB7Ngib<4(rQ}~f(XOldk6h)so)&7Rs#U^gO+Xj8 z_9l0PFpG;4yy+aluN4P*T)yTg+#^srTzhY5hV5X;0+vKsub#_6E|aqA$}^Jpixz6- zdOFXj97k6I0W((g+9HdpvRrf^VxU-3;z>Gi-f6v5<{2(qh0B6(2e}Rr%Rh~_FDgI4 zM#mGq)wtTvo{`OvU0z16OtN4r;pd46Nk>Qi`))A}#|2yV`b^84hH5hnpK8L(JW8S1 zgC-;3>l^w5{Kqw2Yw>=~x+5O?5ET8aP}n-)xqz#mj$+=~ za)(%jo7F#~gDTuf%$Nx$^9&X-fD)$tY&9;|+2p;EB@(ySh}-c*;;=5S1)D}^OfOwD z)h#3}Jbha4_*hMJq~Xi>de112*}0W@>jzmnRpzgFW)oxY9H%zBJL9Y6`*ujz*lBuj=K|fuDeWR3!R5-^M;)1#o=!*reNkma$*h!? zA6U>AsZT@R8a0=Bt0~5_oQ0FRDNFfkc#pi~=#ly?`??Q=IT|+cwKc6@lNHt_vloK1 z*@XJ?-_LDdn+neUGOX}up)1kqp_A17x{(@BGkbex{B5Vw0j!Aep22YPaXrw9eBNKb z&9mHc!2bl+h!3#mh5;Ne3;`JKu92+hW-Vd(b}?zO6^|;6k0>XwB!0IKgWkK#AUj|+ z_8RC1WzR7!xZy55&=QzCcV9K-5Q%wp|85R8qCE_@C1o+mcFvZJ=iOrhuX72 z1eBn6u-i$#+eIN~DCS}66JvZUkUA0ATowyT#FrR2RU3JrRo||Ru zt)WDU4ZmC?lXZkiWrkKGWVG1X>d+CS#OnMI>W{!|(%u{AD=HE{p8-@R9-vn^jL`d- z%x-kg+CyOs((SLL3mg~nX^whTcYZs`x*3(hKJ|6O1FkymG_C_*p*0sufYOmF!N2F|BdIcOLPs>_mpU9b0_1TiV zrh(eXbG!tP20$w7x6M_v6{b)>;)Hu%Z9_|rlsLQ@MjFGYfA^GK3MbT>^IdV+AKBMD zhr^jDQolPNBrA}t;n0Ro#uK6l2{sJ3ae5aPPm8}lI@5N1Iq}z|$+7EJ)DISZ71xqp zr4h5=)6-^mOu~8AX&xk3|n(CsiQA}kc1}Z zZ0me%$-4+bYyBt?v$}GAYHo5ua>A)(0%(F@$Oaa)mFhFbm&K{T-TPsV=yze<1m1B7 z0~?ak^QC;X>e`dZOxz|gP7z@09~lvK7F1<#_I@5Io-8L6UHC=ZuV5j=HE297J<&3c z-Eg>*vUjWd(evdC(1)IxZkR_c4@w=B>p=wuCDiqIjLTPmAVMdWmX~ML!FSEB#W`S+ zujS=7?ia;gfZB9-Hi93IoEjP)uC4xJB}gcjacVRHFA0s+_urK{av6`INP*g>^|^P? z3fU=MS2~hVQL1UtWu2z5J-%i3s%6Dm_fJ;$WA+TOlXtUAapq8xF;U`I*7NeqZSs?A zp$YVHX`Z2>PmrvhUM;$h>-!;-8 zQgkz*wHxVtx-i##Ah1Ti*4Ez(brcxn*f_$Ogt!#~A7pf3 zpo2F`jHAz=(6Nm!WhRW-Fp5lM{uv`25`epEKjwWk7pM;V6NcxQSa8uw_J&lS?+3A9$;Ys9Cn5rK-4?#2!T`VeSKr^?oSD9||U?ef3 zh!O{hrS_+x1)p+Wur9RgXflNm*2zQf= zrBNtzt_MECrX2+H#+RQXv;^2_EbiS_tZ_tsf6 z9}X{XJvGtmC+X-Jv0%FT_9b<$({%Y9QB!xKe%%50{EZ30H?fkg`p#6Z_&!nxsq?q) z!@cDQ|0HV4^XJZ?D{`f)B}OJRa_#_BL#g*k^WSgA?QVip90rZ?!qJ0&Yp=YJqBZB* z!uQDejoXxw?cO~32&KHS?0d3|R>A&vcw0@?YppjO{Op{@brPE-o|&ru+w=Es?W%G+ zJ3V>Iaq17$gC8KX>+`3gQ|O)h4o7IiP1i;{yxb53dUsa{_K;289XS!ryQMA`z;L@F%?T5R?-XjN;;yza5i_yuRYhtO4Agx6i^~`Gi1ph<2vFbxC^^ zXC* zbT|6-MRf>VKU4H=FB{E_&pmv=tAOP!1o4+hV>!q~g`^SmJOAL{`Edx_SdPnWiq-sd z6j4Is;;aj@*vsOxu|9S*u`T8zQAEJ{qxY%|_RsA0(Qr1G{E*9JZBra1&rO>=&(OIH z7yuWYP7oLMyq)wZmHhc_o$q*vUG${>>D-{-!L0~@o)}(d{cS(YnB`jTKSodnM4f?6 z)BO#lUd^+haN&_iS_)bTc*-T*X(-Ln^;qK3cbBSUabMBkW0Kj%1xw?rFWeNJ$&o~* z$6n4qX-v1iS4d>`MU;@>H4;%~#cbo4I|mAuu*X9JAO6Y|KbGf9QR)y<;qOugt_fl~ zlaOpw00P$Wbn+2@Zo)o$9>X+=LnIbbybPswp72!hr+Q#?Yg?o2l_N^ps5RPj_gnh{ zY2sq(oEiY;_~@AeCyue_=3KK-&8TkeDIgMBSPMN?LZ@HzTxT|CQMKEgOdA#aA9$k` z(cI%Da1n;6GuL8iv~XYk2tj)T2G@5Gl*PCIps|qHAnKPeLlQ!}BPA`*`bDtq_Ejag zQ>SGS71y^h#0f9AVGjcg_mw@S-0^qI4@irt_z3uSiE}H`Xb+rHEO|f^LlW)55*(dG z+f3`mhZP=8`2G|KJ79Co8uuI?96YCX6N?y~{WYrFCar$+k%w=U4m$VRfyy{lJL4|& zR~_4@ThH!e*BRa05SBix(&taG8c)@7ZyEh?)GuMzWoI!W&f{IpSP)Wn&bQx1C-8)k zCLHVsJ88o$mMAf!@;lYCX5|+h$nDFx4*7F)W{@VltRW z2HY_5dv=#az|sjlL-`YPZg&QWsuCCa|D)+Tz_DKYzr8~?N%r1K5<)f^Pj>bwNkS5$ ztZcHW>=jZ*gCf~PC89KxnGr?NAoc$|=e_>#b*}e%ulJlzd7j_zzQ5nk{2D`qk9W*8 z1{yOf;H^57;h(_EV=|ufSN5vb^nDR47j=vB$kF+8529PG16LJ!Qh571NXvrUq&pss zcC~5(#}1Nm(3TCzB-Y#L48sWNU)Q2`R`;s|Ef26xJr@?>InKkgo03Em&wJDJLM@OlTdbi5lmT zk{>gPF|3KCrHTd?u+P#&_^-c&t|EK4c-GQhHVK&>nbXfKA0-@Qyh3(C=?j@Jsq!+q zAz&y$a}P7EW-INjTu6PP84Uo>O1N@2dD<1#x6&0;<@_|$P8~3p-2BzFDjNLF!zpW$ zqPun9+5!HSxuBLcsrYAoZVKqeIZ1BvcvL4bxR6!r+VO*q{8C;il3zs1j|&^cj;KDb z_~RQX9>Z!`DE{x-e8%pp;wTimWequVdo`4Ie&_PMDic^yDLCSzd`y}H-_2>gI{SO} zM~*nSHH>4poEmgB!fpdYa??B1a9PSWY4Z^Imkt*{Of!gs6T7{8))kx?&i(Vy#gA24 zM29&BUZPydSBR~9L7G0tnnWfnqeCf};BkGOeQn^d07`jN-nVtQxFlP&=+|DIjxdmr zbV;z8k!0O4ex&zMcEsSu5sKr&4D6O?es5n4oTA!C9(()UiUuwpXr>_zcVl)j61*LV zP?lH7D}`|?9n3+0ReV)3w~IC~MZoQ3m#8M3+36@Hw{|o3ApjgEM1NtD62_E^g)D`B z@?1LM5*6odtCgd2(<#11Rts}orK`#BK1Ij~h6k-EhdX_5{>+}PA*moQd%kN()qoP4}`{k#%c@vICF;Mj%of6=~k*{@t|ADodQ>! zb_(R^M_q`1>T&#d<%DmOt+d)unHtxkgm6HLUKu(LubSY&uT<;vciVk3b*8vx?&lWT zw@`jTuw5 z8NAm16O0msFH&Nvfex_~H3K|%ohS8N@?Gb+0UI*K2P)M7lCiyk8&DlD{=+GB{4T!mNIj4-MRSdeGaWc5KThU*`)s5f4F31*Zv zS>o2qhwhm)rRD2I?qbkDbPD=ThrZV{*$Vm{d-F{0q}4Hcw!4<`v#f@JoGmoRwMRJp zoh>EDKQ<+< zCgZ@A-=u&Hss7>jg}K`;)G8l@zg`pJb)or{a3yWM#V~|R!L?;HJ$0U7_f4ip)r^*N zYCy5jWN(F*L)6V!X<3z=VK25M8I8JOuT|2em(jxV8c+OrV?xYB{XiYzZtp6Ni)Gb0 zb<=P2JQ^!xb5CJm!7$S^-pNN3*9FN5a}1(TLvZ6sTlftUjvLo(o4cu}lU)^}OTWu9 z5pG_EeiT5>w~d=0uj^XBAhYc;fB3^}$$%S%QT9*M%E7jLJJ{VYIck!^92hQLPZ=T;8Kq#6jHq zUZ(@OI=gowc;qe2vgK{o+!YL5L5Nl6mSOaGgm^6l=Sf1^X1x&kQypt*UYDtl;g zxHLRa0W6E)e4cw6^#9CQOJmUY!4&Hu$D;R!d#@3baUqf*hKjF>E$pd&KSLNUd+3!x!Em~0 z=!D``^Ue+(E$g*DNgGdN$^&KtOPm>dk+{J;q!Y<)+a1-UD+5aaQsfA}5jm!xtmNbm?7*B$ybNuEw_2 z8=C*5zFakrx%@#udEW=N&pRi{#TOomAG@w8`C?tla)&MV+52RA{_%J#hj_j?auYjMcyERUPr{XG`w|ao}PkCuWYS%*BN4=2I7CHvyFSX zo@9Q3>W!njMo0YpAINfxcPEmIUwpi0kGTKoW06C_EdJ?Xj{Q|5fihtUX)hm(94086 z@U;s@UE_@RJUKxmJX6Y){!`=#hkp_-0 znbx(~(JuZOXU$hO$+!?WzJ*+_8e_g|qF`m- z2x0Ky66OQFA}U4N-lL1VN1{yk9idYY?`fmH_eQ#>V&>Z#%grBS*&G=<^;Blx%<_v_ebAOcFu>?JXbQN37xrJvTb$zxM&rD zQuatSd%vRG!)AlGyP3m_-^&dbR@3k2bstCiKT#tK`d5>RX5)9f;Rb*pSQy0F|KQ}s z7%sDM87rp0#Z|?2;?(pG!V(0R9OjCcQL)!2vK64iwC;B-1{$4TWtVHpcp zPVj=KgJL&hy=`>jTb<0{VE=Se%4>uH!>F+S3byS7rPCh%DrVN%xfXiGqq541*P7Pb z@=}vdBzmxQbI%~jsKQHNcvy_@N}X*3nZl{ZA5p=v_Jl}>L~dIoV^Sk(If&fl74`bW zc0Ue^Dd4M@z?>*4DW#v%Wk?m-%xC}EWORyN&(ajZ5*{9wH?5BVt4+OsVV56L_aY^bCd=-e4ZDBhbH*(8A{6{%ud%4~QXGBQWwARZC&El(y(%D>D>T>#aLlrXZ(kD56GgBdan^xlylc{J12aB`f z>7!>!bwya%zBN%U?smM+Mo+biiGfPf_*-)qCaM&E8o;&c#wGO#Qm49Qy(>XSKF+-% z?~M;QkslxrDtPV^y1|AIUv-0+5K$?9=P^+TvuKs0(P{%tn2?<@zpSjvSBaLqy>&$O zV&p6B8oF>vPJ1<_OftW_<(l6Hyzd3@QtxLF6Ut%m_-D!^R&cEP$_ej)mgI|7;s!rk z40Ajpi^?f9jD_D_t#YHUb1j0iWG%ddjqC`?1ubcggP6tft~4Y}=G6=1kO=vFs|oo+ zx*2Qf0%eM&-*mqrQ{{<=HxkOk*%Yro^QuYM0o4Z@VQ$FgB50_zvd2SFOD#gA#$jal z{dW5nmf#z1tjator84rKMcpQjiN%91lvdg~(w&l~=IyIdMbVt&!-iCb6@-$eIP06w z`ul_N`a##DK^TgPjOyP;rp5Nf$D`50Wo-PuluuzZiWxB)erUwfM%Eq4{>_Kp>;{{- zS@`c?SJ^qryTqP9&Fu=8ke@{Z)1)n-{)z%1Cg8#!zv>f{1xuxAWVww4_2ZtUgh|s< zaC{EyUyEgv`2gx3$2xyd?~H$RN4@_{FXTq%TMElPh~64lEy8ezNBbfovY|&J824@n zN^1Z}LDZU#;Lf$pqqY7VkB_pgM2rfVN03J{sko!^9qD~0q76T#WV(#gsR#vTCk3jF z=zFQy`U$)Ci>gaU@gw69pnt)kOMGhkSTdD(Qiw7Sr@GwDO^^O6lPV8xRBmFk#q$0Z zt_9*kwtqjT)+zH;OxUA%uM2!F0@$Z-ZelODG zFMjU`iZt0 zJ)Qh&M<;h0*jCZmUQ{%qH%#Xd7{0_OQ`xYfB`wCq(1zO`eeHcv#-PUzgc!=aG_^(d zvk&q|DS8Ro)@JUsxp9=Aq{EA%SOMK6?Kq7E#-l%K2R%Ig>>B3d%upRUb`V)`oDfwM zZQ%!(B>b+9+riHV2izAlzrj5MFaa}LTA83}700PLLzyR{!f#{@uNwMuQz~n*ZKFZ9 z_yqgbekIeTl3NSS3y#Lm+>e@MHfyt~b#_Vd1y!9ZO&;`7CeD#ic!`~{t9$K!j9bO8 zc)}OXu-4HTm_t0Z=LSj0c~UBILFhDhUav~GeGo7QpWDpOEksmnPx6OOT}t}PD&n-A zAWiXD3Vwj-j&fo77F&v+iuwuqu|dAN+O2F`Db0uEms!S7OYf{l1ESgx@}TiBs6*Q7 z^;DdO?AogcvtJ<4kkY5W)R69Ps5y|7kZk&Xn{bIMp-A-jy@r@@LuvDz*q`F5Sq&pn zKQb*uv}6eXh4le}gd7*xJZ>M*Swt-vTyA)F_egGP+9#+y^}FaLCd1M1!MT69;f+tR zllv2OfW0sX2N#M_lSp63CvK=7P!Zv^cN2H`KZwAEMJ~_V!xc8RWPi^arip5d=kO98 zI`r}Qoow6FPHLY-w({}UaQtihs~~O(*IntX3f4 z9)bu2hu-H8GMbEOQnm7qDE)g2qx+<3At~=oQS;I=5&B3&;+vQ-kARVSK08^p!2C$f z8Mcr9ekIk;YVOi33B}YIT~o{y9mns4JopWFuyS&{2m9EB>V+!fM ziV-pi?}feGKpt~guR_Q3 z83{R1+}k#FeA>^oOSg-SDmGDJi*__K9FGXy*r2$5ydkI{!_%wbA+6wq&5JwfrY3^g zg9h3sSDL5asS$t1tW1B7ztw3d+WK=P& z!g%XBxBJ4+Sbk}pJg9~Wa&xKYF*$V|y!;iR&Bm)O!aEtN#qt5#>NKHrPL8G%TeAvE zDSypE|8QW*R{9lk4F^8lT8HC|C^<$cb^RTe6X#V6;u}67u@rB_AvM>9DcS_t1d*)= z#vT<7I-bncP6%5A$sp@>Lh#U$rQuue#r;T5ru8^pI+}aF(W!J;yw4vZB+R7HRp}{n z(YjU;PTqNgdh7p9qIMP!c25!qsR5}vbW<9vl#&67rzVR8DudVsM2XHTaU?}55CTue zu_Vb`1T&eiTY4b;@NNbB>y0O$d{|_|HT&JnW%o;17(2fCQ83-FrMGL2PQ5%oANd1Tbs~F7a?~k3j0Re{9>AdZM5F# z&*#<8ush;6%0z_9xhA7$b!ITg1jC-jd|JU-wL!80kyKm~G* z+{K;z-=A~A^Z|rsmujH6yGhwFj!^vKEuE)_ByY#tPcdCoosxP>y{27l`Aw}TJsse6 zNTFHAH8Ufj8;9+f&Uo(ZFyh(a-~Omwip5y-=!|oYxgF>H#Wk7Hlf;Ps>-*1C%CmZ( zxNE<1WdFoo_g;qTKF!$Vfy99ooIL=s5c!=zNtrTS9|U3ux-3N~cdc^U_062KE0aOU1%xVDPz26?c7NgIgh^c- zVH|Q!?;Aah-|s{2(E$#WHlIBUt%mqU{S$HJNm_Y}yCTRhjn*o$v?^ZwwSG)nn?2`R zlf^%?9K8unxq-gvEDP!Qf}C;+`vZS`N3L7`ji!+^nKcm3C@9!*a95NqZu`2|D2JcI zwV{SOtvnhOb$;GRZ287UPs0Pw21qadQr`7E^8F&D8N~ez-ameEJXg^Z5F^0En$=Yx zDTKU!U!2KN{Kd~Gc#X@0p#J|0h5CDZN$CbUCFUS@QPykQ3jO`>Fp+)|PFo@S{!puU zCBOvI9L4}d;p4xkf6jJpdCn=8nI&V9=0G}EP4l+go3vzwd-jhYguqLe7bHHqy%hKm z3sM^{Q-X6E19vu8_6O2JNV-r#jhm)NXJ64|L;*qw;0>dEXLQS2zie2zll?RcrR=6i zk_w@aI!r#V6`8GHfBwU~f{9uFV0^dW#;GwZ|L!KuF1FAcB)xk^*A0Y`FIB}z zy`TR4jyo0jx8sQ;hTQt>&Pm(Jcv&0uSm+QY&S0a_GG$z-M7*8$!sB>y2iK0v+Fnh; z3Dh@)%+sU}9^BPMLD;hL0r-g^mN8%Gs)-fNnu( zz;nDRauPW+B(;F?*iBf!_3njSEwQ_ewUuoZ;c@KsIjFwPVTztj+`zl?y;gzEfOi#IB zK>W`Q)#IknnjnVq?7isV!c$|VspS~UKx;9@P@Pc%$qZ0z7Ny!3>kCln%j&_4nVJPh?{@Z7I$5xF-mQitv)l?mf`Eda>iGe$|+d@=(#Cf1j?Op>k*5 zP4Dt7Sgo)pi8Fdhw5*XNgVbY%HcIo6tI=mM13$hAo}RA}zQxSdB`$gmQQqwYr4#~F zVe*KMYR!qr#Ww9P8V#}%35c{mBKi`>6;jo`giElO?yVvws;_p^6Nzqjh9{cq)>=PZ z0@*nzB4gA&&5w7Y;=CMi@rfovRf}aev=KkYe8C>sZ~_ zCaeN;E?%lj2q*J$h_>tHI!yNnpab9*;7S@5Eg}aD-}{PN^S99JJskF`g*^h5qFcP8 zBl1#zA{N8cxEJ~V9E(ZjJJH-cyFIccUa!rC5^*Hw)V+o-l30v~QWfyZdwDu8!8Z(~ z1Y}`G3lU68s1jrxM-Ce?7%g?Cepd+#SHedGMWX&4s|fm@`NwXAJvCa6$FBByL-d?( zMtN^#zJ;%irGbM(SlMD*nH%qUiV&7Rn6O|j+>xKIS%`KSVz-!rwQ!6{psJ%h*B^Wr z4}(@VpDGQ-U-CzCIpNB~7yboN(C2$ zG=fKHjn!imji?uSC#U(S?KIr#TrOb+C#Pa)#@&B8<_D=4k-sfSL-Xji7&^e9mH+?> z`u~dX!h>8xX8xbu6lR!!C@12eUucRu%%lNri}<0*u=%odI0 zYsz;M^bB@OX_W^{{*?=U>=*5tI(-SE;kz9Y97P2M>UcnK)KQX=TJG@Uy)o>C4+@GM zq8A?FzXstGAOw6ZcT#hCA%Q(@;cBd!{I?6(Xb=*jHk4}{-f%aOiIjyAAK0vrlp%dar}5xcw%)0RH9HHu2kyPN^6N9QIF66b?Y0m zEY(^Z{Z* zVh%-=!|Y~al0^U6wgt2Po8}E^Msu3M2-=%!6SlaB%tR?D84ukd{*F){XwH(+hWT+J zHV1;7UH_5BH4#Q{s8x2u41-?Rn;5kt%*bk7|I0=&hHw$7uJ(oVgMHuGSY6l>GF-U2 zu|-hWXGK|#9T7<&>mHb(=FqXqHL<*&(!y_ADZp$x-QG{$RY?AZ>6T8KJ+hiFph*Ll z!A*%_|%1 z!JM$XPv_(01r*J{TL}`asqm0UqGDZRqpN1j7Kj$c`DlJ%lF?PCkr>rHBKNrp(Qss)a{INc2LqT*op8ucewC> zwc)ic8+VW>9Gi=_JV?!qw%=V0du&Zq#8#w2`8tdap8fO`hf{DNC<#O#g{2q>j{xf? za(G}*3C>Ln+*YJklH^ahK7*3PnA>prPK=WUG@+;)@%6oZcVD}(4_5YYjXe@(madlK zjM-^UA-h`WuUWOf)}_i*s2uh}UdZ?wl#sjd;n^Uz5^_C9ukoxXezX?)LN0aUd47i& z5g)0R+YHdp=pZeoF!jk6bh1v&DQfQK41a6e`3gmr#Lt}OOng$pF|A6za&2`TMI+9{ zB&|PF+Q-SXtn54XUyp%>gc10&5?kfK0uC(;|oy7 z5>YjS*o5C<8h16X=#_bKn$-CG@>SvefaMH2C44;xFbNP%#rzyoj`E)Tl`kMzhF`#X zNkES8z3HEAXMWaRt9bfCHo%L zKpI!aJRQFh=*XsjZb3@A1+$cKIJN9~8cIVLA^QG0hI1zMpl@Q#`;pFfZC-JKsYm!& zmp?u(N)SGQULdW?6D%THVd6RBQ>;TC)%uotCrj@>eN(Q+ck7~d>Axb}ON)VU22CQE zVH+;01H6nmFWe7b7EsN1?}gTV6W;=neZ|o8uF68_p8Z70Vd~5$9!>9#h_Ytg1y%>i z1Iqr8xA44B9oDwt1wO44DSFWr848tzYL!%!ik$<(1%n>2;0nGCL03Ek$oqYN_arOF zrK%{c*jxojEOCsJ|2~{Mhy>Y?LM93_uNvxGa`MBrTC=I6EzW_;ja<{aj_CI~B4`B4 zft2b6^6Jc(<;+nbd>WFZ8Pbo+|A~xTRoQH3*{9OH8);y)m$jRZLNiTFfYIpzjg!c^ zm*<)K&-X{UFTaEgqrS?UQT_$mu<*cWogoiRX5N27}ZlF*W*2fJ&n!#Y!ex2RmK`@YvFXU{G9Y82wWbsW}0Z! zmcU=tXJ*W{3vUef1$kO}mtQ|alT?0DdLq1UDa&#!fi6{(-v#kWV{lo&#q&T6?Jrc{ zg7PBrX?B`d-{n0be4>5+;5cCX4qPHYCL4P$_oR_X)7) zsx93oO~G+)|5CRbl1>j8-b`8M;-uW`6>Mw%P0um6+CYo95&>(_-~9eIhq^0)6?&B@ zy|P6F`9)LhC~uSIR;*#GsU&a3JP&3cC+}XC1pazNoI+ZUSKt~3w%d*em$D3w{3e|9 zGU81?@{`e8(|&_2Qq#$)5CD=anS-2kmRpLKwX&)E!_ShpK4!F1sVvUZ{W`ygb+7eq23FZWHC?ph}&-$XU zz!Y6aAW;qoj@&k@Vec-=5*J-r5OxZ*J7lN(`&J_J2g#yg@uE925C2#ZoD`l|#df5q zWguMOvbJP4QAZ4?-NmKq8!xOV3nf2^(Y;^ z;`tpOuU}5IX)nigo(Wu%;Sjs{L}2K@yr`Z5 z3)IF0gS*ijEGhTa*yMXPcSIC$gY7B92^pg+-@+_Ukj%!}bOHVdXriG9wWaG$0p|S? zOLpV(9R}`-q#A2-6R!`g55~MJ)_4E-Yb#9*g27qCqK+)LQJw}9(nEF8ILfH?sx&L#@j1y=Zc%=N=LVFMM}p0r>WDd~Q2j2!=&LKOIHK>G4B_<`#73oU z5Pw$)S}cHvfJ_S5oW z-?p60RX#aF(M_>_f&3@SP2M1r;5%M#1Wk`MTsm>v%LsFLmddmee4j4z>!?%3JcDN_ zM#s7$qHpFm773_E*i-a&G8SJ}XQRX4T1GJnC`1Wiax{88@sMl^SE|0P?<3RxCU(Ck zENsV>I=$z(B{G?-Z#sv(Rho9&uga~aF&&346C5hT;!kC>{=!Rzbm4znkN>s!O=+v} zzFG{7uR(xy;XG>WL_!b@0bOs+aa`~B;Y>o+55#{RGixp-uE|I!$Q|6k6AUxY0mR-z znG~%RXyd;4MyG1VZ|`v!pW zK_IozLlWKp&bO?)`Y#}E1g>Z#*!oa1j^Q500Yo%e6waS`nwP%bV!${kG2N&)a(0Kk zX!VP!ONUF$tN#i9)uq2mBr+;})%Xsf>>SFs_<$l=Az_Z1otLC1Y$QK@7db3hypwTO zKCS=q_6{TZM2#vsOJ<6^32p6JZewn~9SZ7^35oQp+@)ryh`as_%i0|nDz2MUVuid> z7;3cSD$XKmn%|6p6Md1zJW*uSYr(vYA-H;WX@sf7)FJPw$IV~0zzHcsuWt}5X&0lyiRvlrQHq<-v|S7qij^%=Of8BaDvoiBxL8Vb8gpnj1eZ+hgYE2VP?W1 z_=_Zw6S;&4j&2J1ew>#=wgEwBTnyNZd!i*a7Qn-gnR_aMhfnGmd3f&B3xmA)+5p&G z&?3i}s^IpanvS_5wY*(s`g5{^a3r=Ve+l}mm2`{enggC#RiIIUhCrac44D&lB3Il_{9O2@oGDePj5 z!~RR_c0$D6CE>tO*7q1!^4K8!$J(01l;wk*9gmyLqngBT&Ls2<>t)J(b`Cm%-D~ur zsqL3y&u&2jatX|;+n2pdG`KBZ;iG0-GgNQtxI}cbuh&iC&YNXF6)ao?%8fISGCSA1bR1?664laE|WcT@Hae>F?+`D6otd5Q`!}=PIZ9pK@G9dl?Z6#bwb(}24M5| zq>NEdJfL2Nr60m7VyiVu#ql>WmAz>AiV;quNDj7yE>}x2xA3Dk$9Vg&xx#>4hTC_a z_o%`Am*-cNsLjkNvl=iwm*NfAA@LqEs>q0`g->_bs}+v_WB0r-@3(;K_vR7NOn#b+ zj~};3Y>0drT|d`A!k4X9-#rjv%a^4C2N_^l!QF;pyqdi07Dj16T; z5j%ASyFDr6ts`s!wLmh8_m~)-q;t&A7Qq4m4}7ts;0yYMFE}?e5OdnKssYStc;n%G zDN+kzxo}^*B%*W%@Q@YI2Hb>5&4O*&=`PFHk#z6jY2E+qMZ!@-9woX7X<4VclA39v z_}hgM<;U9x$3f8$wW0u7ll9GwXeJFb{_{y*+8PXubyDajl736z6zlwPyA5A4-79BcqM$Qjs;^zxT%f!pR;Rr@E zQsF$_sHJ`A8p6>CcZ`Jr#|(J%f+=0c*aj>_4H!j_~?b@JX~%cXFgjlm+4uaFLyNTRJ&A6W3}xM%7|xh?=pM* z{2c~c`+qXhr0GJ*Y)LseVu8T^55ewZzL^q-WA9dNGOpq?LX6V2>(>Xl4(||~E{30? zBZ`t(7=afSPU6HcTRT0&OhU8p9zhzix02c!!V|Jq4^M^u7Vk^ERN?k1eMC3a@L-C_ z7qJ zae0A~8de96NeS&vvqpO3LJzT;9DAKk`od-`&yJ*?0Jt|$H3$9t-DFeufh-Ba6&*!l zqZiWtAS(kMj!L)oIQ_#yX{QS4tXq0Be~I@OKs%v!{PnfPNgmRey`v3g?F0Y95m)) zAyT+h(I7#u*%G_BfB?i-ruJ9&xN##A1&c)mp8>9X#Tsc5J;YFKPJ4m?lW-fJfq!t_&-DNoXUVnx&Q8+7#?yPr0 z0u!(mk)KmhCBu_^ag3aLQCqPcJCwzV_(* zc!eNn)gOPnWFg=cv(iu67AAb!!$Gy`JMa<=(nDC$spxn8i@JFLxEUsHYuAfDk%-a4 z^ZfSVIjL6XU}^EpS6+LOP6O@^*C)t!Ki`HULiWf;5x9xZ6UIbPFhyrMKA#aHzv6v* zV7pPrOkd%2%>DX+;s=J5)t1GXlAo9}G$-U(cb4}&W1H~Zzi=^%|0yCR;pCsr?0XGx z;d#JyOGOWd`&FAn1q$MOx!g~`y^EA$twqW#d2qde#IF7KXF64D&RpEbp*>g7vw2In z!a9fWREUaBd-W{Pg}DHz=7{oPd)Kdo6NhrDcF2BikPMwOnJSY0$M9W*?}&p_4_za> z{4accF((=my<*J)#4VRTvew4bR_Ct7Ds(@6Jbz^a5u}98m5L$n(eT;E^m~-pO_LfdjOxL~$&Gukh#k zZ|!f7Cj=Q`N~?R_bv)`P;g#LTX}+VWZw%?}nG#N3S?p%%h5F?$aEa7ChihizTFEA=`ly3qB3FemK8U}`y5%1!%GFoD{O(<9}yp>GB9E~|*r+>XEKcK%aH zn=P5{A0mmiSRDjCTaZPnf}y_V9~bK>t~krjOgkjby^66mP~$^r zic2C84)KH+)beME(j!n)Om~u&JhO~E(bb4bMuGF&*#c#?7M`5lmXO~zW6YoOvxRZg zq!OY*y3Y^~VamQdM|+Q8@K!USjn_2!zA78}1vNfE5j!2059R%w8SUczS)sEY zmO3t#B~5D8b*`T@A9!QqMFNx#S*8Nja35Y{EZB{KyCST@;4Pj6dX3;)_vDh{ot6vC zFKcb;K9pTS+<=pMygTj$io&Yk8e8!%8kApit;N=&B3fvN27H@BYkh}n~8Mb9pCtmJA^Ek7%*UHr382ZQK&G(KYq_=$gL>c;h9cK zg{+|gE-(&}nh`v^psuZZ-4gcu8#P}rNs9|gMBn)M>8E!|yu1@dP3calI;M%JO6!}K zDIE+LAJnF=C(X^oIB$kH4eS*EQusxw*{(dC>=yltd1i$U$0QxWg`9P$C`|E^L-4@X zA8{En1x`7qur3fwSOx>6CIhHOMBOw5I-Pp5mn4LX$elUw`v|jC!<`TM2WVeZ4_HwQ zxTvbcM7DlClAY2}pqJ|t%Ul>*H#x+wlF10M!z_IIq`;9P+(-VrZ~tLa!p;?j2%fdz z{E?$;!GB=3|1#b%Gncs)J{d`^sHC(XFa61vqJd<6tkiWZk%Mr3Zz6sDcrq|Fowtr@ zE4r^=m3LJJC=m7xl^(jI!^U#{ABT;xfCmS+=^H|z5*JDGRH8y1P2gDbFyN)CMNL`aUtK} zRbq??qB+%u&|Hx9(`8PQdTO9tWFJS8mhzLCHSfUA#%k$l?eaWhlDzr_ z{ixT--vec&T{^hm07&I3#1g^nnHh&nEOQv5R=LUp4Ung3A&T$z2h9VbPu2hT1vs5rq310-~ibD;SW72SDgIyzkllJY& z`o>bcFq7Q3XBVAu6}x#gTd1O)G)Lil`gb$315XH7);%d+eA;QR9l^4~bRe~hVn&5g ziMpTy30{D`iLS-FaqZ7^Z>}Je`Y@|SECcsk#Gp7iL#^{;?43WLhUE<7>bJ+M3-Q(% za2=wUH^i!ffDMZ6OxEIW^QX2By{vSXahka^jHIqsPKIXRdqN1@_|*92j*b1iUDga+ z!0hPy!3W}bYL*3;U(i-8gG%_q$rXr10wf$=gu@DJN>V$}>$PB<9GH|b18rhTF;vXU z70B?>K^+d=WusNUidU2`=mnLDatojzz9?A3G1{IlHn)n|;SQ#$EZO6wV-Yk%I?*1N z71?kb74M+m$|Y1k9UO$E(@(VkW9JV5zFM-#!9$AxP=?R1zY=l7&Dud)2nLbYI0;Fc ztV443PVSkQ+s3WzK)(ooriFy^56mD^79w6a93zNI9qttFJJc zcA+d5L`4Tlq`JLeVkr&39FMM+|1BOJL;1tZzj~iVl_li`i?fl{_04}rtSA1wL`x;V zwh$fp1s4n!U?0p(bXYj332O>Kbl>naW}c~lK5D_qG(>MRr`GYd-lc? zwsBsrQ=Y)+x~3xRlG)53gk0yGke2HAwNHFHUmES*Ab%Eb^26UK=Bp|0E`bDi@O_(x_OjGSHG8r4lBwYD>gI}XCn|V?YEyd@Lzr^Y9Kj~+1x;Jj*_Jh z%ohq0Gd8mZneg5?H2D_b5Ir1Kz~&*9Z8U~>5zwixF_BG?C$q513|GaGMgCzKnk_x3 zVn1P)=6$u6aZX+Ony}~Pyr$7CzSC{`hUJar|4OUW9;NoUL2Db(IX=$6k_S=U z!-FY$FYzzvwpFo1}5hfi( zn36x2Z&iuzc!AJLgz{TAS?waWJz%sz#}-i@PQyA~Q+*!>EJ!o`f&YW;2Zf5u{TY(w z-S64&oa-K+9xjuy{geFCR4cNk^JqM0cyJFJ%Q8QaNWTb42bb%3plzpaE@uoue^{Tr zn=Y-$uJr59P<4KRjqpw+W{7Ta4Gcwy13227F zY5dKUBMHIZmvFN|$9qbgP5Whl`QEyROEm!A5QFAj{T6D!_b?#;{Q7Z-3(ljvUT1l! zzy3rMH!lQGd1EL8M%B@O?kB&K=DpRgKW7_xP8E#M>G=PYXrLfTKM82;iASTYblRt= z6RDSV^S`p|pRC~98l=2NQC}`39Qg)}3}7m^T!T-Ke@!PrhO6n5rOb~n`|+hq)pdts zlLvR^I_{94f|ra~V~GR1AxV=pN#Q;wIQ#{r-nnTq7iJ#BPrO^{buy6Qc=LGTMqu4b zE$NGgj|qL~_`s2%gQ)zdmnv4>B^?n_+OfYP786;FlSd|w(C=YgJHuXadvRnaQ?D+5 zMz1LDqwz;MK51Ew*A zMLAniJj*}u9Vd#3dM0vC^ePDK;(NBIljIWrg&{55IN)Z@-i+~NOIIn=#rQ*MJXJZVuZxnK29O-8~%2*T1HeQ8zMcqRj;iOmfE{%)tQCg=zv=Y*LC+f`M0T0Py+_dwfP zuGP#QZ}kcd>z;5HDIv+heHn?hU{Nk>|6(m=e?hDN`7LBR;mESeGp&C-=6wzu%htx{ z{UP5xs^vuH?3|$TVvNL&#a^~a?abNpo=}kI(Ii+^*z~FTh%^i zDfn@3Jju#v7nt2FiJn^vMj!#9It=3ZTsddi+d8<3;OW2r&iDF(vz4PW!iRh3;s^JG zgFb~l6|AQVugj?pmLI5QgeQ_>pLViB)+%jOM815phiAC&D2)J^Y%)@GOl^cdS?ZxL z=dR%Enbu0Ds{JNz%1PE8F?@o)@zM0319Lyqc=>ReV{2OuI{1fUNe6Y!i0j)%!OK)0 zG5-Y)4sLg%x$9R`{VgLj_Of>e_Z0<34{V+nO7F)zUuP-l|I$$FG+sB~rt32**+xD7 z4*blMDvUf79$k@gJKU2#aNbCkoIz&4ZA2qu4t+1z!%@XvTtoI-RAiC)`T4*qcU_=X zd<1=w_q`vBFRAz3{zmjGz;9+Wy+p%`A4MmP_~L;Ts)x3nrLsf9D*i0Fqjjwg$%qfu zVkmj?yaJ+{D8#!zh|6|g4O%#eq^a^`^>BWc1V3KtPrAY^cBrm`dcA*?LJS5+NHoeY zy%3l(kaaIHo0IX>t)WGa~Qgpb;J7A(hoDi{uwY+V*CKlop$O)5TJn;MD%Pz!b z3Nc02@{ylm>;;4TRHY{|6}*89uhE2U3qMC66c*&_0ez#)|11Wqymhxvi+g*>32mjME1fV&6t$GsGhByGl1}OE>V+ zM!f*diBkmYmE`iaVlR9lb&%>ePke!v6x@}4)F;H~!|G2&^+4EnXrMEv|Ee+BniGFz zu*>zy5Dg`zm%dG1hvfVmGv?|BSf|hBS=M!TUNtPzpBCmwf?L0j?*hXSL6`lVmQ@hp zfNHhZxGTi1=)o30*hG~_?2AGBqARqiI{FN82+RxxeTmJj6%-m48;>v0#f>L)tD8Qf z;CM|XvwCctr+So{5ih`#p&^@_W5@4sEApuvYuH`HCU!f*b%s#=B>l(35l6C~Z-&xr zE47f}^!a;9KjAw3YUewFpcg1XfokvuHXmeyb?-eY&NitU+cgDr5ZHV(njh4LhVm3< zz(nE_1YHk7J0V4rJwhc`_h$v$jsfjY4+z!7&Nl$QOnhowSNb|+J8Bw1yCQJ{a&`cj z|H(ue!`AZv#f=jzQ^hM^62q+zd^S@vq-L?&C;2Nvu*dhYF~V-sQj$2pM8{OP#h zQQ?H-XfhWqW+!C%VXB+?&hHsJ#6dU?B&wp=2R;<+*efvrQqpsvp|%%+V(Pub&MV~5&gm@) z<-fJLlYEi$%2{ASliiXmJ2Ha{KVq2zh|<0luOn#4EooAZd@mXb@n6~YF!9kv&xe?h zs2EoX+el_3QzjT3blRTdL(T&pK*0u z+D7Uq@e^PJHmwk+Jx%-ix+Q<&=Mt ztCcv~!C|8{11uc#OP1N7b)E$rCD_Kw+e9&Ip6>_T$aM@>af}E^@n=ZF!-6bp+1z(4 zkc3~vKL}L9)r=;p{kz%^Nzv&&In&Q|0-_8hB>vF4ZU?LR-m{m`cIYoD=X-19XOtUE zTfNu5fw0%Ox#O4I>KdY8->o1-W5JvJs5D~AfG2_k3x`1mH|LHOuV5|ck>lC78if zR<=KW17JS@oE@29$cP&Hj#(i3(?)2Do)E{-t~0Ivz#hfp^v2VTqIT?g+DbwaL1L(* zWeGL;{fz*sm9M{8)#Elo)%*OJ!(s=8<9VLnx%9o$!>l=8{7Yz_YOC8frKlrpr_1MZ zeMOYdw;~j86K~SAVfXtk)k`b)UOSzAwhviHxuHH-THgzti1@b$-hC*-+6&Roj=*+G zOL!H2ef;;AI+Z(Ov$RoT&)e_fd-_?}|&vOez+VhrX2wD$4nhNIUf^idl=dGrO3;TTC--eZ{BWqu6TJopO!jeFLV@nZ*R zD|YotGK(LH8MVXFha5BUYuI?}UPG{f$%80A^2};p_n;7>Wk^{QmXfJ1x6{jx;iox% z=KxDW@FS6QkJTnd6X}j)P0v4XNV3r(_UMhkuO`?y zgoNBtbB<#xItTE_DY@AsWFXv~%bk8^3eX~@7jEOSiJg|u(zYniC?`w{3~J>n$=@qh zqyCVha!A0b!rwWSPkj@UvD*=3^&$KvECyG{VP@kc`V2^k^GQuf98Z9A`2!rQvqFbe z_p9$!O6)F-ZH`nj_Al)|Z4af5_jhJ?+(w}8hAC+|3|qmHhCzP-Mb{=U zE{on+W4%)y03Q%5`4Eg95x(6X08LnFRs zy8NqDCk99*F)$LS0V4Vn>x!2S{x9GkH-&WiS(gM|k|cr(Z*2G(d9He%cC+4eV<}~u zS7vr0uednpnj|}G1gY591PRgQvVR1d-kp}`YUfk|=88ep3=lPK#ASn9*2iwWBBK9R zHT^)=k<9=_#0jl+wXd?(hb8#+3U~M3k*4^`vDTkW(MOg z(4)BkKvhzRx@$^c;R4Tz*5^>13>9rUxQgXPPef$Cz+{U{C9Oj%Q3&Va+Xi=P_KA+s z;xn$h4!fRsD0E}9R7A)W(Zb@)p=yNy11auH@FfV7oW5#x@Q2j11F-y$2$ z9PHByW&;6(F3DO+JGbW>%qQ2tjzsbEN8iO+mg^L{2$|vP0+%}w9~b#mwL?J8Kyp9A zxI=K(zv_bH7^~L7uwV5ZE)mAG|0sqm)cE!Qm&cx(ou00xok1v&wB)O&oTTlkR_vX0 z!2W_<6~(LYfW?F-c#;Sk=s7B zwz6$VHYc!iOE`smt{lGt$iJYy;og(IT&CnxC#vJ_7?rI05F*QtENUgb>6*ug4c5=$ zTMAV@wDZy$TFe(==Qwco6A=u_i*zvuMD_UNnY}z%-1FWFBOJ~S+Q6$P3e!WYB=-{8 zc8E$!E12COo`+rxZUw=E2d{UZ%v-okT5Wdo*S6sC2(2@*ve!CXY@u%i#-XFZ@av{( z!53f*K4UfP((-k$>0fhh%qBw{k&pK3S9?EDKd@i&m!Ta{-Q~c|eoJ9n^ZKmH^O-ed zha&>*)jf!gRs6{wIQMdinjqsggV-^+hFp$nvulgEoR!r?X&-e~^s%Zke< zl_kOBD&wBqAJ<=v^B(^+Z^?Q8o$)Fj2Gh2SeiC2Adw%Ua(0hLgo>o~-k*GBHn3#Pu z?+91s=2C;n))`Mv8BnsSGcAWQ=S}TKVjOswaC|RBhwxq=RQn*|Dd)CR9T;PXZh#m& zj>w_f!12dLxkTaIefzd;^=J+2v(gnJUcUthfQ6t@=PmbghJkRS&l|<{!+5nw zlqvLTmi9)QF=QDnkqjD8a_y0Cfn*3?#Wy}kX0#Oq6Z9R?p-@g_G#dg=kW+whAaKG^@d&~#erI&-;g1^nh)|#u-`E{e znHpQ%47%^;*uZzS&Vuat4hySv9qG7brF+NQuTQ7yH$OHo#(;#$<2QDIsKlC~`NLj> z|3Wt!@RAao^BK4m|5A=&ACTKPV5#!nzi&5=aP@#$8WCG_rm(3_${R1=pz+!MZTEpa zZ$Bjdl5!gmIVmPwkwp`*>0y5>+w1W&IWF8NMCt z1XWx&Ql;O#c?04h7_2_v=&*@%uMNZZRU{A>UwlkRE6-aYn6vRASbh3u^CkdPT^A(e#e>^+NYSqY_7${tBp+RI2*$!L(`JMQ;;|M^zG zFP`VVuj@RIx#xrb^HcXeWaZ~S<3<)dAG6h!Q`e`{?S`h5*$kZcYj!lp3UO7xxSE@b z>@cKgNpZc;`yzbLQd#pVAO zkc@*u+sVmEfTY=)R$+5UfBS=FZNBza&LmB`@PGAo{nINJ{3`s(I&_90tVeZds2+aU zfP)s~^aidA34LuJ5IeNtDc7#;!xw^xb;IDBnT)Q-r^uhTc%)M5*M=?#SpEZx`w;ox zT@yQr)SGo!FA2zEdJRu<@1_*%uD)Sox-ixHgJM4m`y{qQ;e(9%rGUb%e6hNPx%aRe zfIW+#fR7UO;3|AJQkhsqbzhvwtXU`c8{9xNTc}LV^|Fi$!huf2j(@xR4(g^lI(;FQ zGRxRIQnPx7m90M>zp{M_k8v=UP{hC&9-od$^$#TOXzz zaohCe03WwFzo5Pa@;Kf2{YyeSH;#(=6{iJ;fP%oV?xV7J)I6=USh$j=p(<=twsV1P zXYcv8eG^&3u5%eVjkToRW(d19&(u9e*6-P50%Xk{3mvi!RVCLl;&*m^BDZBLms`H~U%Uh6&pfq@%ODcjMcWh~Blrmq2ucyc zx=oO={Z_Rh>`-Ouop!g`=*LNDB!2o79x0S}K{#ZbC9O3|!A^z=L-`sqZm75N z|HUB{pS%WdEJL{5@ez?q*@ZMBwU>}IYXos$<1MWlP9R-!0Br-!mq(+0EWL|DF7#D~ zU0e^9dF!6LJU!@}$S3s|Vg0H}w~@nCojIvuuIlya#;$$&3I=nkPmcu63e6r13)I=S z+bzM5f0fP3f)t5oU=x3}xga0_1B(jlD*R?5>N)5^st`y~jEpM6ae(<(PZuenxv@Qy zM4c(*aUpgm>p1-?tjW!u{Bk}yXJ8Z`yBC%_@D%x`B9t+QM1PW;77Qj%x2>_v@1Kmq zh!Xf{#@5h&0$3Yjd%vB3f3#rm6*hgSb|5%o7W7=aWx#$4cnEq+QvSs4r0E?h%~La$ z*jnFU9R`1-#5^u9-0-6|*QXJT3E+#z4L{fZIMyL&Ih8XX)w;RU47?lnveT!EBc{V# zobg0W!(zdk7RjUrdVeWQFVAPKE3GvO2E7~gg;F>FPVWaXl&CAj*E6mtU68@G7 zBtC(rlF}fzNSE53C4JZrA|FeYD-UT|BN18t^5#2)x766|Y#Neq2W{E=R;xvrk6HMb z2uI-?=3N)y3qi&!CqqVmU2?~;6lX+DpAK>2IF4ou{*JG0k(Qsgq>ymlfwPske{l^b zpyAx~qpaIk0L>?ci=#%}AUR727rb?!>&lyxzXBgw7sLiRLNZ)#l1u(Xi2ao7tzAQ| zoHsVo{F<{BHYS7xvT9z<|KB?~v_HbWH|;TVCbQV=shAVcOMQPzKD)55$G7gI1PkRY z#=skO>6pavY`3g0?DOr5;k|%e2-w2QTE}&wA6tC`4!R1<%Ih#3l3(6l@6A`&ddrrF zNEF6Q65~=N114SL=cpCpb-02>tX35D;1M;sX#+4Qih&ED5{o|fITBiBU{{=i>(gQ- zxhGHL6BD%#ySswmaUSL?s!5Pqe}%>7p<55L;P4w6 z&UhXqxRQ_%>Iao80@bkJ8X*fb8oD+3I=_L3hv~eqP*Fgk6?yDyJ-0u?Kj5HvaX)5>cgq4=muM`cf{Pk;5?g4yvSe{&ybmmB?d zJ20k_Wceeqv1_>0 zkd5}_gjqoIm90BApr@!)7q*M(X0*%~`B4jzA0nIav1k{O`Xv=|h7QxLiFzG=nnIf8 z2H;BjD2a4VKYyE&GKQ5`^Ln0RIB|bRaXE!LMjb@ta->?T`Rm8Qm*$e&TX1D69_vpD zBoon3J7-KTZkeC_YJTeCV}K11Z8sIoa3@&31zd$4Ysnm1WnU2)m`T#n=j)vQq(Zkf zQQu}i5kRrU|E*4NlK?HUQ82FCZmv#lb&Z$nhH+Ji7gyRh@YolxXDfFcoww zaa+C&PMw<#nt7I{Mhe2C+VV&}%a=hTHi;5pxIEedva)BOnnoC}_Tg{=OiS>b-F}0E z@T3wik!GymUaD8t&z+%&{IC0935eKFGO7rcyR!hSpu3O1+gd1HeqO;K`;)|*UhzZm z^}6GvdT*#s2Um#jr)U<#BYC@Y`I}mAS5--fgvkD^6kQ=VKTv8@Z8*tQdI5cF1RTXR z2O?qy^Cdj@U;Xa)E(0Vr&_c!?O5ptGU!KB%LbWl2qSsjAP;$4*D@6{&_?&Seb+|>Q zBl81qisZu&PUSpf+6c!{=Qne5`iLbQD5b=A z`%i@2$fU|XOU?bBNp09*l_5Md3zbsmF$iPBP;mQ z7|RX_d7#3==8hN$i>ah1W)9r{mqQ|q=O&1Q0|{g-lp=r7cp}GblCLeA(qWLj#Wb6# z{ByJ2K->dG)MY`ykTkg%FcE2Wj;54<yfuzWj$7Td7CnGytnj=oEZVk_4H)M%o&EiO!Jw@8OSt~ek) z6g$c_zu$XLR={_42b@^4#6MeqZ4sL3EYR(BnA6H!k{yzPX;$Dg>OFyff6$U^09v*Y z$!db<=rMk2EUwVk{1AjRGtB@wulRUyXdnC?pRy2|9L%FYPM?vUI`lT;W^$GR1XH?y zxO_?M^7P?E5F`F`Xb~DcQhY+G`R%ghZh2S>q3Y>Jl>P~IXcqP(E?MU288nWu*vmO@<=8wU!QRe_;k7j! zY6YT13y4xtMARz#>mwL^K^WX6Qj?++;x#dEQa~Q?A0ZNms%U%+>Z@ZBfd5a^S+iLR zC(VA8PE6%s&J%o8jMb)&7DERIL8)T5=wk7l(6SMY;Kl(A_E3D!Mh=$I?i{QBo&OIiCJ zB6U4bA=Ri3QQ5Qp@Kt&@7CT>)*S~Q!kaVBAspgjHU-C<$dOIbYF+E9C37LM;X-=8G zsFV$46M;QXLFH*ZgC63)v^B}hK18!YVDNt%@*Qk#GUTch4va7eZn4*;%s0IN?Jo2} zf)?pjCoN7F&L}MZ=d_ymZ!1qVKn@5 zyouI~CYcOCGEf?e8hND}rH0jfQ}MSRhU&EnaWpq=<`F(B#FD80?fg1~RZ!N>V!97y z8T%PnbQ3o`Oojk_AoF26KJ8uU2EGg2V(8p%k!Rx}g?sJGEQ#o9oXtvBg~)KP{K9>mokB-E@x`EDxVC~4*o#_TH%4Q@yL%x) z!h(vRnC(W^hIEP6|E5R6o2a>&>$FEZwhw#Z?}ZWN{JzZG;Z3o8&lgEsY?Vs4om$E= z89?J4PS}y)(hy2x7HOk=)87Ds2$3MfPg_o518}va-cy89uY2$Pj31lucM_-ZO~Z#PFMF-?iT8yo^4dS!LMNk!p5-@pk_UoJcus0 zJNp91qqs^nrYt)kfyHh{9P6jvb+hhGN|0CiTR}dig2vYn{Gbfs!Q2GAtxQ?`pf@Gr zQ%1)8&Lw}TIC_7}V>u3DjrK4%V&Z?|j<~pq=b8)IqfA!GVjNX-3$h**MiRdpxq&?(5!x zbQiK?AGNw!LNBlguMtx1h@o7SVqRspyY?N{d~HXW?B}zQrxG>4cyJf7k7!n{NIINL zcZy(6+-sI0D9KLW^tj>xAe-Jr6zk$|y$m}LXg{4WK0fp$@F#yq{xQmjQkVIyMM~`p zMyfO?kAs~mJ%-~Oggig&^AuQGi-#)^ve7dM;$;<}z+j$g#Oo{1XLXBBY{F{&LphkW z@?qq5##CJqs>eq;aXz6Qpq3hbB^;)6E4vQzK_Z28kTn1bCBqVyB_iHb1tN-yXCaLR zoWK@Mo-F=jxo_w*3iunS|9aB&-ny{s z(1i1IXT>rJeaoLJoeD!9lSGev%jNw`?1h(YKx>016?PTHBW|>1&36|+r;I}{H#Xd> z=$RA9!q7+63ug(YV@%hG#q4rz`0lOw+_mp3o0;i(mu%IM~)_MLB zVuLn_iWR&8$Ve)W^Y>>%9yOs8X@Zi}$+wFGIYblxCl0Rr8x5+hFw!R=9q`S`tn)#A z%$+%{1#*^8&(4Xd`^)_?bh$2HZc~l+hx4GmHojlp6!zWMsez_w+$b98JtDG!cJ5@@ z4sw&`!Q?3PgD1TS2Q`^n!e`H&%%y=p;)(}+BuTe+iS#F3K+n_6LxW$%JP&QEk;{Z5 zf~a)InS1g2jmIS*iAPu=@^=Kqfn-R%hq0m^sQpAR)dt*A7)PLem%c#zEzCN~NdbJu zLHMC?Cb*ddjn7agScv|P5DolRzEesv$o8NftE0_dUOqm{M?wM;bV9HOiJkJ!a(=Cl zs_*|`drM7rVe#N*-2G%Ot~+5P;mMb-{9YnI&(e2$e9CpZz7LO}fjKLFNqBA>(Lklp zT6X~6KM+BsR&Ovf=yyc|F{Q&)ifQIYp!$PMuvQQ>E(Jhr4$}~79D1iq$&HK5bN z@OtF83mZF5VVwVmG(a4iYWy@*gdSAsDkf^02(6e)I?9GM?`(@wN?#tKGI##RV7zz< z{#-j%6mJ`f-+T8DO<@{vwBW4+gFlo*VqN|U6Agfh?{994lV>Mu{HYjyWlJ9#It139 z<)wv4;Hpaow;PBTEYv9C41g1BRQwtvr5Ndo(H~g@C&ZQ^a%}y`mPU@!z7*pqkpb01 z92RBVees%CF@BF)Io^0K=%8m-{c8Rnp0IViQ)rUw!CetNxzwMVU z)Nj!(u|W1V#}2;P(vl~&XLyR-egBC(rTZv*dWWcv`DXOTQS*2}9>Bl9|E{*K!uO2@ zCm5#%c+WOxq;ZDtKB_8^KaU0(Upuai0cxT>*JcQ4^v;-J;FJv4N% zjp4P@)UWx<>#S2zooB^ZW6|R$%1$2)%~_#7z6+5y_@6=k;M)6#j5c>qPdwp|oFMa$ zAJ~~AC6MjMcja8+L}AsjJOlSI8T#w2b9Yilfu@fw@$U}-Y#aqm-nPbUad{W)&dR3( z;a^=+LxHw#rEIl7P^D6UInd8^&;PXiClXj)JPQpr-uy4v)bUDpb3@VTtY4=j#W8V( z*3Idq)XeMoSW)9tNjx`7ycWM1qD>$0DG0ncX=d-9_OaQYeZXPoWdRduPIbWrvgV_m zk`g?c48)kV=Zp4G(>W7#m-#NS2L2tLTJu6;m%^)G=f=~no#^!0^f1m5E{ zL}tVA63%kS6``sp6m@Woq4-@@@nh>RP|3wv|_Tub^RY1C?gKg7HXHRhmJg&y3mU5Xm`8I4>c}j_-n& z5o+#?X#1N^I6QGE$N!a+;xZQ<#qNkxUpw~T+=)yaTuTC;p~f-5qVnX48ehnS3X>Qj zYYjw}@hih3OFW#}qMT>~Oq_3eAZXX^H+;-jf;EmFJN5+?FH1lhk@TG{8jypNKRXSX z7-#}Slq`=MQQvD+h6nfRXvradvMYJrY>swM+m0O!43YdGUQ5BXKVjx8u3^{@*_f{& zrCQwXB$R8DDxbr!#KYSL1J~|X4iWnpdhCcG0nDZpVWE+Kf@aMdCi*RTHb?3EE!8xN zt$&CoB|r8+dfKtu`G|NSic7z0=zQWj_XR|Gixi~S63!Pir7=H?P`qBqDhrGonJWp zp(D{HQGTuauY&+-?`VM0%nC-$b(;41P+2-GLVwU_(K@QFOM3Saas($l_RtF8Nw8d% zSfq8|a^vw~dvQsT&l{<#x?2Q!yNmS%^L1u7Y7;CYbF81BAScYk&@Mdy`%v+)7rd-* z*U&|YCjeDbM71lZQdJalROx8ZI42+@ifNm&(y6yU&hD< zJ=-sg)*NQLJ^JlD6bD{P@O^X8lxo~G@aI91*XY-K69>|}1n-vaDbr|F*pj&($1v0y zS*&nrq@n$MtcYCN8W7=Qw-cfLgnG6@u-%Pe*Bzp*TdGks9qLD&OvlweCJd|43T;C* z&;Fu(LmQiy7(;->GmTQjUkYgmzndo{M)y3E{)Sbi?gxP&XZHsc&d5bYO2iD0K z8kKl(yOaQs42bZ4LMkTKmMD4Q&WTqGkma^(x&$DGk>ChQH9k^JST38+a`Vl85xlw$ z`H+l{u;C!6kY82j4AOWywh_;6 zSgK(0_dY;H+&?JJzM10}F5tt5gclRB^ucMXN^4xPElBvoMf_sC1=Le!cIr~EaB`<> zJ%Jv+xqao2YS*N1im~9^8#dKNzrcpG0E#z=tum(NYyBj#$S=zAhBIV`ajF;Dcjy_H z)y|-#M2xNdz2RJDr>5$Rdij`X!p) zxP~(d3aTw3JG>21Z~N9Nf|^(%jp*-FuUgiFEW$ZEGQHxm60Pkl!M+!|3tY(&Kkty{ zZ{M-z;W*yPtz*b)Ldr9gl|D9zfNDL>>GZF%AEJgM02 zTF))g(E0>B&=l0S-yh7&{CGDy|5IRx_P)2@e_sY|56c6aoAe>x2XlglLe*nEowPy& z6;XB0p58NgAJuPvetmn&YN%ILro_#dP~U>n1LV?s6l?~j?B!#=_ioouJ@(MiL$%%& z!*LAEvwjmxtGFnUbxfrE#qVc~Ixle#>8{T%y!X9=$7N_$Gi6szcawE0{Q5vZ29Q({t!0Jj#j2ya}mpo zWtd5r3>Ca1J+uDE+ewh8-bxvsb>!|z1du~;>Z!s<-I4gZw{+f^Wr3V3*J?$N>Pup} zKnUC8lz^Kzp0&>U<7#;LA*nn{x~RTWcid(C)4}m{cM-ckDi^wRtDZUb=47*U@`UEpNiuT z{yCj1M4}C4+oG+~zeIjbuAA`lf!8WeL6vB~Lv z13Q7$E&wrdm~h}$ef|RU1v{f~0Uv!7w#GcBrIXTiGE2ogRdItGq>`Cw$zuyKG?c?M zpQqct>J?&`&{pHz-aG336N16Vkm{hYU`$K6d&_bjs%wwk2E8G%Z{I%FSQC^tyiMi7 zu&2VOfrkKpG456v@vn3Me*k)hKMC*Nu>`HX8rxyvx5zYV#KQy>$Hj+qY3ky2+O;C{ z3h1(sf>>j9gPDo;oM%^y;b^IGb5U3Dv3ixd&O|YmkoPd4=H%ucSH31$*-Kx%!XDr{ zQttFT-|Wry7XQ@K?>l#LpVx71jm?GEl<)pgQu(4<@m|KGS862VjDTTW4_Y?#IYlaL zb8kX@Q{b_L^~V@j#S8Dq>iUNfQ_N;tgAWu65N{qaupH)jM3^x=rn z_|IzqsVYEqB4z;82IL}VkV=)8n>)L*W~Zflqv5)B<+|`kK^m3V=cg5N@!)m2j;kcS zeJYxNb}jGz9SJqIBQINy(8fz0r;lY@_HMj7sZzH>$_T5{79b&n)lQkmt-=C^msKpN zxGX!xSOlk37I5(O4-8~2S`Zt+aj09enTMX#;TeN%9VO&2QckK2Nju8CLbMJR85;~b zB3CAtnz~${JbgNO(W#1zl0zNcD!AsnD!N>YHV=NAGHLz{G^C(A^H8 z`y(Pj+!*t26{t}NUo zv_ba^sqN$t3HJ?gOyLzQ$v=muub7&S^k3;Hp+~_vnmt{J4K^ulq%6(JbeH`A{(1JS z?0u7d2@ia&h)i*Kz|m615V+to0J;6?d#F8udIqyG;ldwX(J8YGb0iP1)h`R%nC~OL~8@)kUVzK3U`>aB1REzAuzF1>z(rM>}Fu7)o`^3)%KE_8N z_2hvlK?PHI(_qv>yfT3uuMYvq%~dK8Z38U|q1`m5WC1eDG-U^?uSdPS$;?7)dE12l zB7AP*;>YO~MD6jibsp8EqM0YM%`r~VjLsTqh!QOyYp2qC3gm60(Nzdj$jj_WDbT)eX3wZ8OFCm_(N% za3a5;UhG(%dQnh?4K;7K%fgPEf=gRt+Gi5A~oVBfOkQu!__2r9@wY*DvB)ogM z&yFp=8!xWYr4n}Q|8^c$-^Kh3AKw5c5g<9oMrSD)K^4io!91ge%b3XPha%>|<;@V( zMBism)bAMgkzg14SBkf0|BoRZ{O=Igs3hGXR4m9YP(@pTg9DJ8=d+6;O1xdgVfMb# zvN2b}*Ke9j$dMy`0mLIje=#kZ4sej`toKbWjY5A?-^0HRl5lu@Wdo(cI4`wu&>I|$ zXnw{W@jG5h@GV295QAmG_o*0>jf;Zy$9(^Aoe^r)qW7$@#v(zK{2*v7K9_MP^ZGG( zX8^_S{K@eY^M(@a4^Js^xSdnsLu7W&)q0Ovj)*XZgkW~S9e3G}2NJ1%BbRL?*bT!+q>CgI zUjZT{NYJu`5y*;=OSD=8vvSX*;HEk1ACa97*?`PTcZofvFYl-RtpRJd7R1*Bmx++L z_%v)D@4z(lnRlgq59l91hf;c1cn<&~m3538oLjdlSkRVoW6*kGCra-=(fS=FE9lC> zEkyL7;JcF_O_{}y!q+3X6fX?H18l-b=r2fGDcJlg&Ka-1+$(J9_!h+0Z_p)(CWGrJ zQ6J_F)>j5L5xjAHkW`C4knN*vC6#}cGtQvzh{C99cM92nutNGhKez`l??|3-mWGDe zGNnp&Db@X;>yJktG7j$8)Vn&U``F;7EQhB61AA1Kg_OjQjFj4MW?pvXeeyb}e^^m7 zyPINqV;z1aBp}n>W)h)V?T2feN8Ys)$BXpO7ytii#kcfPVsm$B2~j8i_}a*YF&Jtg zjKD%S6f!%yCYH-(f;=fo1f3MA{RDVO5EC}rluhwA%e_4Pg0Ux&C4&iAVe-^ zV@S1Ck^OhS#Gkbr4`n@kzfk_T%zUK-@p01^fn)lp;Bj0BbFK#E6lNzkx<9c3v;wD! zGhmuNiNzVmJ3&Tb4(j_MQZ0wl5~9Ee#5+#kv~uRo+Pb%hNwMFL_dNg~Y-&5Xz_&kx z+!6L11L>ca1jKHcOW=3*dAjQ!9|j|ExezrTi(OD~-VFcDcxDF!l?=@jm$?;SxYv}9 zr{(6xUKMD1CXny?%ThALTfok~P+ybC84`op0x#^YtCSY=C26UprL3t^yR5TT4)k+% zJrY?8)nts-(&qYu*BAQfJcs}T7!FQSYW4Q^aw~bIjJ^Uso-jga$9MifL>RWmD`Qgq zNHW8@28|=^yjSz{w=+_!CPfs5M#@!1J-Ou4C11kuDPUy#fs8Ue;8$5TmIIw&H)c%Ov5T-j^XJ^Za8w)i)ik!V;i9;5=A*0ClleuFCzj0hHA>`3mA^KyDJC_+8`$DVbuEcA2JKM&kWZ16Z zYDJo!yOsJWIdP!B9pkMVsos;8->lNK%-d4uXq=db3rbH!=F{vIs=7`RI4~+qq6N#n zoQTLj0?=9idK=9pv$iQc&N7J7zl+;D>aQzoGx%1|&W%bUo;O5IyZ992+d1gc$d{gfsjtyGzDuWFS`)O3Ba7-svy6{5EZcfuO3=v_x2 z^Rbq52dPN;e1l08oBT)MJmA_tK*a@e6IrXo)1!Io)zcNVzTRPWn$vRc)oK`%t(_*s(eJ;SyweDr%8dFu%{&bcG@7?k=3JVhDTNo8mz5e|!4QClvTZ@(^^CO=YI zvBR;^E%?JjhQP7ZPQ~n?tEYf0f+|CbGl{m<4O>#?ICfn8s>iM{Zzs_X&H7^j!lX@4 zNltikTP|;6eg@+f6?e3s@@-vjPpg2N2@ZWAk`FLj;OA%JQRwtinmCXe)R7?6DEIk& zol?>MgNHq%S)LaK$YfCgi-h7)UwClE{^ngfUnib4f!T{!{_vicQF&x&@8OdSCGx9z zs7|o16DNBwZ9o7#{5`(J8HrV(^sx%mff;l%XmZjf#z7Q6(4-v~>8pA*kBrXUF71@& zCR*Ddwq|D*r^MmL_v z{x-u)HqjaT{=GxXr&~7Fv6tf`IzAQlxv!4vm(+uD!(WN46$~hD3F?@bXxmDU1&W*G zkP`Kc%R5woMm5?W8lId=6CXH|Wuw0Ca-VZ#9VAuAl!VqRsjKDLq48TX&z51g6Z^iO+VdW+ro6WTfcTUKsgr zWc!U*hHZ`W3T5P^kAe+7#p_1o>ThgD@8Ksw3k>4A_^sGYrJJLOwe`roTAU_58ow2y-ixF*$_~6h^>gL~syj=YY^mev-+M{yxf{W*DkVS9Q$OPfX4GW>+zmjC?z|P zros77_APnX&nucLcDIn@3E`3KqaSH0GZEU}wQ?~o`h_QxM~*I93r5CIJTaHheQ@Hl z28)YjkvSL~C??P2McWnsa1j@nLbt(*KzPCl4c?KYl_!0DSMlkFJRKQ3f%rVI3j^XjKvMbjz{U2XeC|{VTI%YCpO@SrY~1dc)zTP zWfXj`M!8!~r&G#RbSx(BR#_)qg{%63j5zn=sd(<@rGWQdO5wrqYC;@}D!+QasXj#S z2>Te$9Vj$4H=!A)MkNATXTG;=Vv!zKRI$bz4~F4!T;Yy&6mY{u{0p8}j9p4w#^`V} zBNFQZ-e=tyO(S3=m99M_-Y`T)WB))XfDB*FnV@(ZMn2Mkg^{wS z@XE!>OtEuCXa=kCBgUDOEi=CuXD6gpGR&Q*kKq(v$*BEt>`~L?$sJN1{I$zGG;IG;`)C(=Rs8ejJ(VSb7Lc{2y%(EauVwJrd|B zZwFidmQvm?V}CG=XExQ1_)cJrhrW_@c*~=@&L_1(#hTx*D$_N`pqzyx;aibPEET(d2;*%MqtT|FPSM zgm}rdAR|hx#pjXGHm66PE1xyG##RpH_rQ}U@T~UZD)N}}C~d*gH(K_;)`E7~-@zU65_bTEoi_j=wU0*Z31?SV2Tc1NfCwR@0*?fw z;{%3T+!O8dxDRvWR@F))4D=1=c?EJ}MA2r2-aHQ{|n_x%&!qE zD9ci94~&w@ED`P)JVIdHBsO~xit|E8LwKpw>jgIx08=zc;QGmtJhZV8^4cf0ni z-&}3|LjKG%Zyx3Wii&^@})XJ@q*8e5phuGF?}7%qB;oSkSHwtGUC__ z+^`UvXNAYd|J%K9-z7+Uuo!gm=zK+tW_FDB+&sO1Xji_oW@N;P3{+Fr+YRl=>CO83 zY41o>NqE5ZsUY4dD+=k4AG*F%eBM58CM7GrCr3dI@HaJO`Hm$raQ+?R0blET(6T{y zA0^6TS4E|d^QCx8rgB1{Wd&FpvqL>lMtI)LB_%7X(#G3a8*A|2&kq%K-~%$8hGsDU zY!zO(tbh#jGiE$vc;QaJy+gOKgL=HGy7nrtz>|gK2UbUXV^v+2wwKbRG%GaP=clGx zpdB=6^1p5zDoI<1<+x}=^uXhYbZl^^3$m5^dO)Ac)KrdJSB80ND-fy$=wl!PfxXF% zk=-?mWL6$vR4*egNg>VNl5axC@2*8Mn7sOaTmNfHbi^CzDQcxB1uk5K5+5|*z={3gdit>8NYBu${gGZhB{ZKdnY z1-gxHVWj>7)}ASW^*?f(YCIVW@{Y0chBhYr?Ts( zIG&QRX?kjwTw1zTna!8n6RbfLG$XCGpH<7H?%&1hE$}#?%#^88mdM}qUN=0CW7HLe`UE({E$%0 zzmC3!H%BKj#T>;r-VXQlu16gcGcl78xv0j^%@+9Rqq_ox+?UW3Gmh1c=`2q!8+RLW zTkfhr5O@;4BfKuCHDafw#HxU==~Kx1H-L`04Vpe5&7%xr6|_jmA({2Nj~b!dL2G__ zA{s%GC8ckM;%8N36$886HVO7)wU!brQIa%kU9cKYkn^6b_;T6!CHN)_R}5jG%q5XpZF zZw~&QJpzW|Wga&!S>SEVR^dI}iQE7Tc5hI23UCz2MwIrr-+lD<7BhO>Bu}KJ^=koW zaBthR{*J<}PyL2CIAh*IF!S|5Q>^vT4&BDBuQ!MmTYM~5TfM7hW?$Vp3ROz!V_stM zpuxa-A4ef)uN-MVjp;GODS&s1N!ZEhHA9DUsaYrQLHl=G!L|=VLXOv^{wr4N?>C9> z(!OdpvDn(gk+12Y?QvH?^eB#Wq<61?07&W3V-o4?cRAlm zI@}!Ki+w{PpZ4Ay7U10dmo*xV!v>h_vT@Elvg;kdVQ?JO%~*h9cX!hE_;eGF82&w{ z`5dXrE;48VZ`!&;BY{!wp_owR8H30!7SA{)Y%YWo%33}2IC8k3dnli*170(+ljD_ zt1##4{QCZati^bH1o|OvZErdXTVv8&qMA_@^;eB3tC#N7s71krbr8rp*-}e*! zpyAiv)q#rx6$@8^un{G8v;M^c`iWQ!Uqfw|F))}h^HHAp&d#;rlAolM^DTAdf>}IK2b^~*iW;c zC;cXzcEDRBMsn|CN0OaogZqUXDiA7s?vYcvPajRxT zZWJjb_qt11vsadZD6UW~5{~7v@Nr(39--=bzqOFEn5yaNl16F&-P@V3=$9Nv-v~N@ zR4_x}CDr;spTy?IxIGP$`KMEF;UdHaKQute(+jUGWT1$|KJ(!Lhoo4y9U@qIsi3s} z$YNZf{{7uOVtN3i+IdYbo1veU!&oLMc6o3#57B(BhCAmItR#hI(-;Q#(8q2(8ZEQA z@f=Ui86e!~9Xxv9Zwf{_0ctmeT=?JgCOW&A1!zA{O%V$;4y;png@|(m0v*H! z)P!+&*e~Cv$p#p!}bF;BCvpi7S0`V*!_%E(Wd_qWof>t>5a3f zBJ*;Kj*n7ANgh`BMUbFlX=ti%b4H>b4g)==Y-V!`AiQ{;r~7knyLQD9bR~S9FXw-* zs&<}^2_!L6+upP%LbHD2>4cyY)=(&cOZO!0bw>fXLau)R?3M~Mx zF{rMVA>$@V_}9#g8uI^^TWRyl#V&CahqTwwUyRVx;y4>B$Q*e*l#XnV&aGGQ*t^3E zhDh-zN91zl9bG;kh~ppLy-2-q(nI}{!#G@VO$7!W!SI2P;_C@JWSp-m z=sAMJ5Vb=NKdfOV`Tc2gAkPaIp>5>`6-zFSpqh!qDg^bIR~^h{%NlzU@hRz+fH&EC z101IK>zZL~fQSv}+lhPc45j#J!k$7fiv+!oSGMI@Uo~BzS-+qgQEL(OZ6bL*tH_-q zjN|h$m@GQRiI)VE<*$VgmzF;pBFMSYG;2)9QkuG6Z{n?7_Z|CFIBtn+5%1-_!)rvF<=)rL)WgOb7E94qq$Z@2h2QfK;&$69)19n|~=TY>$X=wm|~b z{=<;R`oy#OL;L0 zVQ2SUc(1B5GG<5@PWI1xXx(a-!c{V|)LL+1-(72ISx`3`!0nGh#NamADc*;NNG=eck@)x-AcXNAbcU?DzQMI z&1ZfO$If3@MmwZ=s&b|xgW~OP>L(VUq3+9B5!U;#aYClJ2u2uzAHv*>X;0!s(gbLs z*y=-7Hf*;r{Ki_o29u9L?MIH@UZRT_3JM56Z4qy9xL7bf#mm0LrW(B?@9;Pvs%7MX zgj6);X1lW-*cX&+X8mB;bP0DI} zl?}h%sq#CDO+9wj+SYZ6dPf>8?($?~Hu{Z?==I6Fc(PV-K!dw13uNP*vQU(vhfG?Y zT5pdQDP=D*{eXJRAkqOgvt69=|9wZ+cD*XRy9TZdk{RcrGi$2h3P5v^`)!KL>;I6J zHUATxbJ(zBb-`cm0?JZv2fI%Dkr*$p#<$v8Lmpe+lbUe`2KW(gKY_vGn8krXasT9B znZ0u9buZDxhaZSopfAk7WD26RV22a2N)A?>LC-IOU5)Z3v<$CD3j+#gZ7nS={rq6h zTN2!!rXVUj5{k50Kbv`RV;EN5wTo$Ci;4bf zM{fapnR2B-SYENfCq4zk2wDfOE!?C+{?xCOhNYd`gy{0vdAg|kgMHY?_Yo;~X=plO zzM?~lB*tQg`>aXwmG|j~7ZkuJVfS4{>I{lKA)P|P3pfdU+T#{5e|*NNh=N!G!bP#t zjE@TLz)wY7O!k6t&jIe^`7Rd8k*EIfWwrR|k29soT|{;*{`8*QbsQwfx(EHYcX=dx zw}#%~*MB`Ha#^CRs8?IM-#;zYh;lJAGJ+~yQ%q#zRjM-vY8=-Ca=v?0sIj^a1u;`c zV6y{F{|Dlzkj;M=N_|p;bXa7+%Z=Sz+|tec*B$6lMuv5zWm#!yDQr5(gI2*E48-hT z2IY4a{SUJy?s2 zfM^qm<4FJTR^qjujqqXqE)RKQ{@$O^wvBinuZ7aVV??Sa2fqYvObB8z(Ba~tu#l40&XW^jkpFv;b1JXrlNIf_d^b1; zgoJLI{c-xiLI35;B~_D>OPHFPB2?i> zB(eS;`W82V-2cn6aVL@?`>1Hth(9tQiN6; zLXan3e3Y^Oz;`K^{ed9JyBjdh3LA+)QAo51J*GllAY4m zr*>m}AL76zvNa`1UwPYi=boppP84^#@m$G4?ibXG3F(v%kFe`4s%>ZPfqDo=dmHyI zboE-^+M7Rc`reJwQWd_WG^4{;v;_e_{|vNPh`&A#>B6i(KF{VI2R%_4X2>U&+Y6v^4-|}M-ez5-!WNxnAS5Qh z9?5dOMfjDpoy>!Nj}ZZHfFk*mujp-iWK(55i)WPL=$?<2GUpwGAMc2luVFA;>@Ltr z@EvTTtQ0XsxF_h1-Me>xniSX)KVdf_6V1jz!II}AXOT)%K1+z32}%*VJnV>s-?N#F z!!rJL##p5B@W9CD9?M%S_KXImtih=lLM5Luv`dXNkBaWp=%DDlv{A}oa{uU}jpilj zpmX0l%lQM<#(+KJm%vuSe~z0EP|X%{l@8sH<&BBE_rqxq;*#Z>Ells2Ch-S7%E{G{ z{y^+3V2uI_hw9v7kT&qElg*68^Ko(s0EQen9u{O%&iacs9@Tol6Cyn!cE=xG8!x=| z4%NMyN7z5Eqz_HL!c^>}-JuO+^aB7Y0)ap_VxoNBQ{evhW}9N}@pAKvNjAT*e_Z^s zs{MTRNWx-4;dds_I z2uEHp)Nir;6u>4#sQsv$K3nL1&z2_|AqxgCZ2Y~LBMC~yWfq$}&Via5X$1j-ihd=f z-nswG>MOM;JmJXb4j|i=RBzqjrgbo=O3&}6I@9>hqFhti?cKRNncByfC^ar~hc{XFg0U_R# zF#rwC`|4BjJhdjx%0fqXA7Ixr*Q`A59z}LqX<3nBkj(Axc=Hmgd4l|M_}G}1U-Hb* zxDReAoIc;b0=qzx4i-(cc-Di}+Q1r?K@wv*=H33pY|F)FZg)P;0?4Dh@~(aF?CPhY zdeu9UIGfwqA|}gzHj#{8_qu@Ce}XJ1KM&tNcKrD9*N&;;?g>jFh~aG04b5hjlI0Me z-uG6m@}cx4fqw*d2wM4guZyX?1BRZb*k?%E?R&2nZ#^1L`M`M2M1rE`g?D``Y0))d zE$A_>RcE=G-|NpuMj+q8z67KE-T$YM)OkH{sQ&p>8&H)kcx0HR_;LJTS#vs}d#1?LU~H90Bmqnq@qFIz zzIiX!(RYm}EVrm+F@mX=%wI6&FyIxgi+MjRawY1(y)Qj;p zGa{pR*v}ml0FH|IFc6OT8$51d!*&@;)~?0KD6i^6eA75>0}?ND(CY!v*rt3EId2A6 zI<(Y{XH4K;#6F7==q3IIhGE+M|6m)E$n3*cA>Um{MX121FX^R_m;d#YU^l`Kaw6lF z7NbD=6PbgD&AqQmeLmi{Vq_paS7W(1LpJ9IxwTP3baX(nXQ1^;e29Yy)j1%};0cpUjc@GyTwnBV8WtHRGHb?4){Dmc2 zRbF@ncw`KTh3rmc_v*^>*j5S+6ybmKIjvP#7<38iAL0f)BCA_hZUpU{WWQ#-=&mV! zB{^|YcqJh-`7Dl)#S|Vls8M^s{HS^KW#D~sdn4eM+!g!a8J1jFfHz@*26@3L8Yfsw zoz*B2*F-fF(sm53B+%OX`85oCxo1l;+PG|=u*U}Kl-apawzTa`FJ>^2MI1T<+ef?+znwv%eSv6edP=SMW<;JAc`?Ru9*JdYbFYPD(Z#qAD~RQ9X86}lx(blT>&H%;fu?3G>{ zQBvvUVZD~^yVr8b&A_|Ehr_F4tsqJM(^6)*OxV}noZIbL+D=NrIp3)}Whsi+l@V12 z4j57Yt+@Q4d%TRPjo`M_0_0d@?W8%gpXwDqS5ax+p8f>Au@ko+lU)w{{M*t!Y#=f3 zqL;9s&eP&2J2(>soCPs*RU)4X``n4wbuQSKQ7cm8PJ1G?i?3yDHp4DNw4D8Z;bF~a z)##uj)Op+6ajhBAKe5v;mO!@fojZ5nm=af4&$oBV)VZ}aAwtrs92QS!0QtzHVK90o z=pVTa+NvF@egsT|aSYXJyM-i+CBQEHSl0U-b+vx{7drjWl(TCW1hrNs%EQ^0-bm!- zp8J`@?Jz(*`$676}w+j|aY3L*+vRYHmRL*+B? z7(AAa4^bcoX_SCNV*732zjEBj{yK^we}s4H-5rOQ?;Vk}vp+;|tFjR@@OhMDq@}@g z?QFALnH*%i>TXTD_(*>3x7(LgGmEtKJMd3jKwB41hGL0u_owM+B(GXw8$t90d7yz$ zZRwG8m`R1UUf|3_>ne^v40;9o8 zVt(9LfYgNN4E{!rl5n<_T_63+zfz`IH?zf`$SD3FOV=GwW&8i_oxMY3kBmY{GRw+} zjASQNLI}wwj*%IaY*`grG;K1cR!7o>+Vr1Ez zWx5mxaB%10CdW2#3z`?O-t@8B%y%p#*nONl*rtabcV;E?oC{po*5UC}qbB(qP%|g^ z=LUXVLu(+qY9YwMd;#i2LM~gR(?ikaOewpB#Uq>iFBbo<`Mrq}glXV~LvO~vUj}7M zA<%JP0K2Lb(Ppcx7e~}v;o-!Oh*_g$(kaPPk=Nc2V?0t#GOt~WyND-EWS%bJbk<@8 z=Y}y01!4+xSUxQH1MOMBv`R(E+r*BAzHYSe2}TxTW~B3DUECs!7DMTHt@2b3x6yb#;yo!|{yV}X+AcWy3Yls& zRv`?;JfoD4PnokH6+4DC7zEY5P;Mf*UPx42Q{t`gd#yD5WxQcmuEG@Q@6eavw~ce; z3vms3s2}@8#u>y8pY%*5IMPnH=HPTseKI>y?MLb6*Wy<**G`6QBI)P z4Y>BsiMo{g3aOV)neRl9p;h!HR8P&cV^brp) z&@QolVR4Wjt;JCXN$z<=ralEM%PpztxX#z>*0xofyUEbi;OqHBCU*IJc8mA6O2rks3Ih@ zm<%TP8(Ur@d@dJ4$}4W@GZg~=C4-X9U*0$HR|&v%|fT* zu}PVZ8l%5UQeOA$ZUyzoF;R=!IkD^UQU8RfZce1C2;G_~x&+|Ll7tk5S$|PM=5l z{xc*dK`_qZk-V0hi)u*8S15Zyv$@oH*Kbn;2YQ26(#YC##+Q#=^u4oxa}LdVu;l=^ zxS)26+d{W*rJA_o5rwXI;WMR+_waQo*1WXOoIPRA^Z>{ByO%Q6Pu-|fIWE5Ia^4~T zOmQIQ75jHj#EZ^<=Gb*uyp3XPIOIj1f-=}DP_2_R%wiEr0n^F*z2Daqwtm(G49LNQ zWrwd2*ErlHI5w}SCWb<#1OLf;7}tLPTt<6A*ZuN45`xl>=_Q(iI0q*gu>PBQ@_9OT zYWTG28S}VltxFIz*(SgW){Y} zE#F!zxy;UZW%3=8rr~lxxXaZJq?{l=Yj_Fv48&=a_A?h##a?B&D4?=yN}%OJ^i6C1 z!b-raVIB7; zF!@41ytjMBPxRgb%T{O%NjWobq2h{R-)4*+KeydJh=Fer`ZW$R(|{n~ZM|-9xRxpe z1^-6Qk3*ETf7f604Ri^Gko^nmlrm~-S^f#=xfe(cTv*eqeID4UiA9E(e#|A<@$P9Z zC|OKNJd_-IAVnqbIKPTlBiH1#mls62#0uIQ`_FHGI8*%>;U6M;RF2kVZ-dqIh=2}~ zR*25rwvWu8^$iRRu>O2LB(~Z;euRU4_Uo3>N~nvMEOW_QuSb@dY3IN1csP&jZ^P0M zJPDj~4h-)inw&J}kEWLZBKkhxFWj#~^rx?pjgZP7u>sxdddDzJ3b9gV+a#Xgum0jk z0FetnXBmZ&q)^;+2c16u+nY39=~=K-A-ve`DlW=JJ7d#F_cQhO7_qyNTKZo(wEeXF zTiV5hU_Wep9C|E@)*{Ma4*NYq0eK@|4=1*uHNHnZbN?sy|KUI~LNDz({aPb68=d55 zW@g4PUrS{zBFD&I?N*Y)ro7of*cQYxJOM5eT|*6GV^Ha|8YluQgAt=zjqw&kR$ID` z-rXmX#n2^uG!J_53E^D`a~Dx11yY9a8{T9-5kn!1cAff%SG{|5CobCrs2B3?l8qN) zEf#ydYe&jM|B9Df!Nuu^mEdE1}5xDlj|518?au&AhB)e!NX!3BkIJ0J)cURy#m zcbP!ee?;_f57jDJSonlz>V`YR6~2DvFZ(PkGZ^l%C%P9_*k9L8kW`9E_xyA*nP*q~ z$kdifgx1jn-F6$iBt_|Rjxs}+h$}j*dqn&!QcrJv000i|8!W(!fr%pO-aPt7PNA&z z2ik)6nv@-5*Msi%kzMj(SF8*O2r*PcS)qFP_SXNR+-NczGcG={uMa=;d%hS=r;Iz5 z8HQ<&hZ-8v1@#YJjgVV>Qx|JR`Jtuz#JAsFy+@u}(x>M1>i=;qPMy-7Uht;;s>)0w z18yB_1`U7FpBo|uyver#_+U4aE?0^mChYlPpW7M8DL*F~JY8cu}N<{Wr*)GiKxMfO}sJw6yaLbJFgZ=WL08>4KTTPyN zFl&WWJ2!zt>r0)DOwAczAN?H}T*=CLk(ve21fW?EC!^?N`O8#xZ%6CSH^68Sj*b%T zdr`mLYCKlC@89?`{tiJ3Kj6f>LjLOnQZ$hx_F#m00~rg0P6>k^ofnI?qzK&8S5mwj z7wV>@&rkm|%IZmyHi*B@vR%2Z^jk8PwPpNsa(|Aj2T9Mdu{EfS+#f{VLNpT}V+RI7 z^Y6s4bt0B@?SwaYEKQn--nx!VwYP#Ron9_GnDei18zpCNiW>4KQJ5rOtzTk)AZSIo zD82KL_f|sNzl76g>}*av!q^z8kPuH2V@W3%X}9Xj3x@ zKx_ONqHK(gr)q7=zhyH8^bhvY%`D&dO9}dP)TQ_N!`>@oZt)(>ld()Jdrht~^KF8q zeg1o~DqhFjTKSRPN$l>>3o)G%#)^|lnpv$<#4v%#?a?O}%3iy-?91yxZu$Pu%@sTZ z(O=mZsGOtIOQagllymM>(DVNHJ1FrN$zTm`U5woJf|CO#B?_hKnp>*b2Tv?_ow>e! zEPmI$O%ZOs=dd9KfRvOsuJE%;FF}i*4`^5IhXH1jdkp?RauMOVTF*Y3yi<4C0!8U0 zub+d!ZXYvd8M^>o9_0z;-fGpAlzl%8uR`-YQ zuoz>^vPZkS3S?suRX8m{=;$R5Jyl?4|(bw3JqQf9uz<0l+n}d+1clAlf0{<$e<=gc`|uQa-LsUs%6~u^VXZiUv>8d98DVZ#zS)t)gScya8Bn#d9@NGIAt6W9 z1GC_CBvKrJ{)D6n`zf{HCCBG;8AA}HsL|=#iEYg{lV+*WB8dtOMWZdzJk`#h{>};e zo_C#adr>4DCr8+y=5g@iH$IC3=Kb1q)@b}#97M}6!VoQkrpPIIzG>uF11o?!*Brny za#Q*CJ=}^dMeY6n)M}w^CaL7DHz<~d7W^SJP-Z6TXHGs3TE*@la8-Q&{{4thR_c*2 z!AY5)T9Vl28|30+e8MSEGAlbIq(xKwfX3&*g2L!su??)(c@*UQ$_$KDZ&TF^n#f&^ z`lhk@Vj_>mJmEkPW2m&N#w~j5YYJ35q}b0i?rQf{2D%SzD6vaCZl^b~94tF8&*aOg zs~s7+xqNJ|r0e2&+!lLd#D+qt-YQD5d0*U`@d7h)HS{^k)O!Pf9HW zWEGr6NCXtVd3h;CwCtri`PK1d=8X9@*3@eOmlJYy?#Z3=O;WJ?A(cgEDGXi{B^hwv z;47w!0-Xb6(W_pmEH6Y+NlHov-4XfiM_fu%1kW$cvP1WD2tXjjANn+0k*2ZQX=!Q1 zSQw;_6~{6NaD={a>C7{jtJ(4b)4qhadlzl~$Lec4-L(H%cs-BV zQ0bah$SW7Xb&WLb%(HwMreEH24@ebR_)xfj$Vj z8UpsP0pbhPfOR}GzyCe$v@l@`pcK(v(7YeMwLw+BKhS)NUM!dBRSLV;J-V5>zx8Kw zZ}y(%@VxA4u@}Z;-tuQRe+Ra6&zPs~c29C1-35vpo(}`(NQK*P7m(2iJu5N}Kd=9S z_aJ*7!Ca740qM+^+gKiVBuGKfR0v)H-Gl-o0_t{sQ&GIaO=9E+(z5Ido~BZVPYxQY z_rl$TFw2p%DTQVOw8D}XZ9C+ejHII-pAc9MMJSM7dY*CX77=NN@~Qc-vpLsKjJzjn zT_}H(7s{|DN73D2vDvcufeS5kDnXoB9GIUiLVN%4N-ai!tJ{r9wFrIs2ed=&(hr#d z|4l9{XMKuEjUu+s>n~n8x7*9@t({r!&wu4h$z~JprDS9taH?K0)VW5)0O7q=LwFMY zYw7}81ZAk6g0L{Tj0WQQ%7(GFB_Dn;0uT=_1POv5_uNkH-@_jZQhje%X-cu4M;uf2 zFpQ>U$J100VoB`HY1r=DQZhD zKJiYjY9mcJMpf7ltCm;X0#sIrap^GsNV-5NaKc7UKgYFHhy%1xeSzoL=}e zrvX&l0s?lz6hXZb5!^fSRLW)F{Ji+NN>O2|Fwu_rEq~oFYy)W z;g1^M4E>p*_QSX1Q12rYFJ9kWlks!m?HoiCh$sOYEp`aLa$&0b5PJ8cb_%gSZl_K` zXiB@sqP&K=vm8G)asCXZnrZE-mrXg6#*x!L~vtQWGOPeu(uM8XR&cd zA~~{hbI(2)@mD*E$L*u{k!|WHWJq5y(N{6h=$4ul4|_8=9>1DczGI6|<>5?K>Uys= z&6!7bs2`E%9vB$FV~Ul~!h-do7dU={$49D%i{EA5kCk8W87|PUEqjZ=^_dwrhfZa& zaMp$p`ZCS+&eOSv_yWB0D0ud58HE^aRl6!qT*2WCf&g0rrl&hV`fn#dV=UygluSxMFmpo?`Jmpqox;Md-K9^I?0TN$_7a{yAJ`sAaL%w*< ztgaoIt~PpVIjWr3`lgrtpJSMnlR9X4Xlj1EJ8}M3s0Q-7tCbr6{g$Ssb}^+1f*!}x z0KLF2?cUH(p%PnfBN%(=0 zty7LLhJu$zk{+*??=qjb+x)}Ki_4%HDh_iQ&pbK##+k4PQ9L8v?gWo~x& zsk+gADo)9%sL%YW2Hf{+Ho-?6LVKQ1VgFCR+kJr!YFSbzZv=xT6;d2gD4tp#yGpGtAY&o z$W_D&uYaE@DT!#LNV&SDeobOlAIIMonDhwN{OZLZhOP(X#*^g6ilXGckM^#Wo5hL%>MG*OTvV8AKkh)Y>CA^ndAl;L2Xi4?|zaX1#gw#YHO^_lq z%@DE1Z!y$4V^2gzOIyn)?>ZtOA(hoN=|dA0aYRM`Dn{jT;OcLFnmy6puk!gx+xBb5 ziF~gu(-s#hHjW{0WYay7GiPTRPp}?(>{xHYvkVf~x zCtXGq+MP7lQzWLn>HK~G3+eaJZ5ai~0Re|J%H3RQ1uJgm=Jphs>~&3gO1;jg{A z--3nm2Nb`3MMW>tSg-nXC?4CQD^xPzGjWhQbXY9eqQLU_QKfsp!|^s@Pu{!9Sb9*S z6zTThB18<*5$LW|-91ZoT|2!5b6GUAH)>&a@NNL3+Ul{=i_V%``%G%Y=I5^yGkxYyumZNY;0-xXC>G z#N_0zy6MCKDiA{T`-HqoS7bYbcDZ!ZJe>`WKM7zb<< z>(HbR@)Bqx!w0)r$^b_?gHWB5Uh~85?&Scrlkh47&F+^g&`lrJ*o;jvl3SRegEc zTqy7K^D{>r6cX>>vCZ72^-iGK0s1EB_^GE~__vGN(Ct3Nbvc&p<_+;Z)0E>AQlGR> znO8Gx7Yfp>bi{u{4g({fiadrH@DWXal&%|l*PTG1(}Uk&?iu*i9`dO*I^`9c+?<^w zF-oVP-h?jA(!fL~#(Aix zuOf}Ff`fAUu=N~EMqQ@4E_(~+~ga4aA1+)A2 z)`k{?T!#t?9G~kgDu@Md`Opvd$Sn@7UI*rR0h7R%Sz|kpUm%q7mnAsbk627yL`srB z$X~pE`3EjC>YCvSQx|b%_$KR`)ao}8S$sAprlLR}A#t`iK(HxWR$9`2^MQyfYWBaXfN(mwcIG0|8Pf%R@d!f*$Rb8oERHcZ?zq-Owil$kJmE|gMp%*}O zOv*C6YFOlsNo4`zL44;O80D`U({x36M!jNGmuTnat!%5#s5)OQOTNoSlg6#U@yUoYar^2_@=R4>8)p%TSUAXO? zG!2c533uak`PdbC(~tiBJDcmN_ePMBu=wrOX%cg`fg_=wvH?v7Mn)G@_>fO;=$5em z=JHkiSz;dykW*ObUcY`FOHOh~oteh$&^g7tSYiTiH>e)Vigf5OV7|mjOLgFdp%!2IAOz*VrzU{&%GGiuDGQ8BUi)xVDWo4!}6f5u>EfW(t zD6<_3&FVjTyS>?29st6=Y6Lr;P4dm>|BJ8HMbmxnzR;rm8t z<2<(E6AwKik3R+5!vDQ%G7%yc;}HYM zx@4(mpdbTqCMUOy+3{fLro8sfnqZ%|#GVsyt;QRVh?%4=Xk5ysT;r&pO3TV><391! zU-F+j$|TAgDA|vWdH)1d4svwYpyE_*vQHQY`1T|oVZv@!8bC6bsL(T1a?jpEBK|AS zw7|;0So53z@&9q#5*y!h47D@;`k`%$UXM%dDZb7)*0^5TaV5@$|D38%X^!dL>A zKOJ>u`Shf10%#A?0)xz+liBxh51g5y!&0CSwBocP*eEu!^Q?ZlC3HHtmADz=(87u@ z#Oj_sj{vVlrl3{(%(#B0^vk_96hBSZZo53pPLtOa|C+OF&%Y&VZw5|sSvuPN?eFob z1D0QfEMdZ=>`|em)D-~Wp_@MK-2QJ;dr#Zj>h?I8x9*c{k2eo9s2VCdoTm3{vX!-L z@bAH&V-GI<^fqUQh4jm%M|OX~j=8zJr(cxvxf?BN05m{R_u+HrZ(RNyC`@*3YlocVj#t3)l4Ui2A^X;r0=qNrz>5ZwfHM}klH zo#dAo`?H58N)S{*=KB)nO=5uOe~7F6DPV1t%Btnjg%^1W*C38B1Djrt&}LYIeo&P`7#Sn7Sa@e!{kBY4fRg^s(Zx>$6Gor z$)Ns$hK0zMRGj}nUqgJ4#NV!rDWz+^*!u$@;*p zih>MqRfDh48FIt;Hqu{njBi8!fr-QM!fPcnzL@oh{=*+KJ|wlteO@E6>tTPm+ltkj0|FkK*?0=vf^@a{xO5+ISBQV{-*`XD@`1J#23hmx?_?YI_3oqNf{8XJJ z9m75p{CPM}kcMW&^}_T4n)o7gwh6t9C@A@_zVp~^W26+2yUMWBAq%Basea-1O`Mt8 z^HrI+(e^NpLoq$+$fWWQ1zu5g+O>)+07m?7K` z)8FR(sp3K>@uOugKDaAGULYPFd0d@TU8a9yAyF4FMW9HNPeNMHq;>bHNxz2Xw>JxG zG^MX;X!3kPh#c}lqw1MneHuqJhx>!;|I}-5yB3A5kfshx&4&Umf@t|K4WDvxfBc`_ z5Zr0XMfSNl}(`uF~Vu_qhC!73;-5Sd@qP{hlUlUmzrn%Tcy)zJ%%cWh@KCZ)_7gVD!jE)tX6>t;#8dID&#^s z^gEV?J!K3AFlK-uz8kZDO{h#dZm6?PrdhB7k59;QP#7%FgeUDc=+)7Vx=GNhnx2Htq9xg#>3DwQIZjy0Mvo<(A7p4Y`1McU0 z$Yo;#ZJ}ODx<*72n6raz74})EU1WA))`JQUSYf8UDkB$y35g9mhT|~P1 zk-A0L+Via71rBCuLjfC6whWrv#K;Jf4%Buc0*fLpk~GcGIN>6Jg?Yuq^JW|6$ZiEa zdl|1!X^*hOyt*K@=(V2H$S#bXm`nO7cAHvM4Qc#% z?+P4iceUVn(eX8eWSsgHpmS1_{4iNewBJKS!gk1)2cz1v_@a? z@Dj>Rgb6Oj)RKnrwD!XOk{^PuFMD#4hrWl$giWd8+?1`8@=XLAy!8I4ws@WT-1_t9 z&mjmzcKVK`=LG>d<==JQFeRU`ut*;{D>m{qM_)^;M=-`b)gZ1e98W49PEgwulC*cv zK1b?s6=pXmY9J#~EXJLRjy8pBRjU;?@wO+~KeU8vtx@l~Qno^qYj(}5H-5CqA`O# z^`L+$_7pry&LnB%B-3ZnVGfJmK^`w!txy7kxWUPDD{>oqAm;n%jd|~RR!iNs^lrB$ zk;#e6b`9eVr+Wny6{GqM0upeoM3360-ce8?n@K}UJqY>eCbL(|Q?IbEG1EXWVJ4x7 zyjv(Xcb2g;P#N6NPZy~h{o7102Uydo6XQwh!_J+NOwrNn3@CWfaiBTtDEUh;$KmuvQWbk%o2{d_Z`bht_OG>@|7#frE}xLbdf`1U+HR zZ}8{9l+cZXy@8XG?_xsdS)`M8sgc%ymH;IH;Us5#0n^Ejz7v z$d*gwK1+P^*L9ZZB3bKALw|V)U8GQgYE(kI5v99${p-q)2u^z`;%|O|8YNJ`JjsXU zwafzZ=2z=KXu45DKoP>vMD}v~pF%SPvk7z#)`B#Y?3c8d? znQYTV@_VT}%@9yKmzwSw>R!REeA)aZYraBa=O)ySpbFb_z|?#Y5KRrof+b&c)|F}QWYQL!{aKOy@Wb>WMDCFapt)jJTctPNvfjR z^TS1AE5tLl*lYDZ!)nZ*PLZ6X{$jCw!OQynd7lcX+0JCgova$V=kVewZnQosEVu}mO4Pih#-*xQAfzO$n zqnsZ-4CvLk8MISrMN?)tip#h{jwcqC!}3R3{4`|CDqcB-Snvoahk8rVFE3q$CTjbvJmW!xy?YdMvj}>V*+li#Ve8pPNs9!+;t%>~(VFn%#M$EX9MWGtQ zYjPRJxxgEZA``4g`5)X+X9>?a;>`HT^+IHe^vl?=5#h%;e z(0gbk5T1d0f%@VwGX*N!7;>7N8jDK{KIzjwEEF}E3Mh>A;5u%YxH_P#AnN?jHdAMb zP{fg@KLZ{SCVK|CMRLb-*mBEjUfeNe=Pc`V!5s)lq%p z{XK(F2RP$WUa{=eZq?8$GA1m&urw4Dr`uNUX{F0d&~Dz9{v9@a?|k)OKL8{|t`dux zeg+dGm1?KkiDDUyxQBp2+h#REtGGkOUTRoq|}~Cx<1Eb=WeW? zMi8KffstsdU8rbsBH`jHrZ~^6?vQ+#+%5>C_C{&~`)j#v^pZj8rQwJ$JYw$@1C_aZ z)Y(N4xB!@L_j}mOTHB`VWYWJ;_=i99o&+z%giT9g)D;wk$YQ^^yp0MtF;P(=RpWsD zVGZG!J#d(a?fsE??yx{QYgS2zP%(3>$fB#w>sOt}`sfY_V)m3_v)4x>2 zFgocohAjt2-Hn5n0ug275V9cZ_V1zH9$M0siwZX5P(z=Dnoo?MM~LDWv0wzP+xfky zfQ2H5*lTcU`X_~5vYuS7ET8)M>tPa(oKznK6~1XC`$kOn9x3k61(J5)ardmYs8nWS z-xQ_ifTkeqQ^$@S10{dlvxhX;Q~JwSpe~|9`|A;8{|9+?+8NKI-jqEQvX{D`ueIxG zwa}{7)alqhP27K=mQgNu&@IAOY(nQQf)bIgP-rH>OtbknY%H#x+MFYa??A&RI-ZuC zUQ2uSumA-iAxV?INU2L{k11o1OvF?>5n@`L{e$8XqVB42Ga#gQ6{#X^{!S~CRO5l8 z=Wr`ZpXc9Ad^2kWmjyW(L?UT|t!50ceq}ooKhRAeULYcxz$?BcvXE1{ zQT}qwbvI>pM#cpwd9QY~OvR+kd*1K~Sh`PrJ~s2kh@F4q9hBHPR9}QfNBsfU1#5ge z;K^Gyp4_PtJgtM3e;=I^_0$Q9)vA;dN6Z;S)*jbO00#|4!7X~f|5UEndiJ$Dxu!+2 z)K+(&-Dq5V827I~fc?&Fg_^``k6&@3H!+J08K0Wt}=Ikn^ydrMdv$fqHq z1O`M5G@BCi5maEr$Bo6h^&$e|JX-X42Be=zXtcIhv9@OWIi0e&dFFRlgL?#Y=zx-1 zCN(`stfV%7u1vO|NiKHuEA&Hn@C!G1`<>sHo+C7oW72~h7z_1R*jGGWAT*zk@yw$S zFq$+^uZo!MBvYbsx_Im|It&a zyD{$-7CML`W1Lc`hOR>=g6<4HLJ9=v~Y>`DWAeUk9^bGz3m{cG4g;F@N_E=+C;J%5z>488fXX2xru$N zVeZDS?z}&+b5EFG8+}o}Q|)SzhQ1BQrzC#cRecc`<(p~l6kL|fF~qe1ADZdG?}G@w z@MRwSOP3q3_?J_S>FxD`JC{L~DZbJd-lZP20*Mq2xhRFj!^s`}jxeSCr_^X)lPDe6 z`z|@vjDpzL0HP3R7THXl%di*+!^ThWkO-rN#3?fq00TN}^B(IiU$xyC^tzYkkX!=q zT)K8o(##*3iSec#tZ!BL2~-7ztB$&S#3qJBC@HtX!sWTy^@w_U!P4l@(S8s>jN7W9DZ?gdNz8?WK(J^RS6%=mhg_A#mbUy&&Z-E8L{ z4%w^=#d(?~$wX)lNvf3Nq6R+Z?AV_Ew(mSoWA9pNLD{<^LM5~O@a=V{k&N?K&!-t) z&_aZP{%BMpeKy03G)J8AH(%d2lXwYc1OmX~e0ez&lSb=C+Nq3Q;tmCz)l4pWA9AqK z{w_x6KN4L{js*p(E?hA124uhDRC&?Dy5;GWk52)9BVyfZAez|^Ee1ZTJpjQVAbyC; z6!;h&(dd>DmBW62K7qGxExJ2oUh^NHsJMDzT*Ekvb+gL8c1x)tY}y5vsCl-`BoHP} z9;peM7`Ya7S@pV1(L}&OT`DP3G`a&sVd}X826DS24-v6|b-3*sGoAMP?|9|lcX3Y= zY9o~JRIsfY4BmfE_vzL&&*dZqMDNv&LhkorE_J6&TW9&_OAo6AW|fDNqhzaweq$eN zbn9@7JU@oV1{_M{h4_N7RatuiC&x|(D$HH#@`uE1OmveEI~VrOK^{FuTqKx;hXf5+ z7yD2#{1lzUL4x%cYgQ&sX8x#m{$8Yes6C^CndAsd?mFBPe{oH?HMlI|NHC^s{Pm$a z^bM!YQ-4oDgEoo^Z@6`oCUE^6JpbwW=E=mMELLkKzJ1M%gl; z@AP$bR3=<(!>+=TP!zvHYI34_80Y+DT=`~BAlQf~CA^7`LlQE`XHNRk%rqC9No7If zT{VOe=p%NQbP*^#YIag+5&OH%HEu0aUQ?EE5G1(ZPRB2GwwT`HYl6&N?&c<1(4=$i zvGd1T8C{U*>8FA!HDVw!@NW!}LB9)f;;$O7PFy;QR%ydK(e&U#osANmf6{I;`YXhtd)mqqah($#3NF0Q$JVAho_<{xzUr>rX z1)3rh40X+|3%#|1MG@<*1|K8zQyW1Mok+>S>qd09Ll~nkqWx5NmfFISkzJB;bLDuI z62zHfmfJ1XRcYvc=@J!|AWGper;?vx3KTN#!L3MZ$lAk_l(;L|$eH&F$ z{Sqm7=PZb+3$oeLh%z3-afuTSDIB)d!~f-mP)f-?%RJO`Y_`@N1mQIqTeya_6xgS9 zWNq)y;IWgLPRjN7Y?GS~c|htm|Moy=t@lf_sv)A8hH9tO0ABn1L8~rqXKGsx{T#bM zv&uY`P=P`66WKR1V1=upuw9u;nTCA^tH8vkVs>qslboUiaxUD~G36h^jmx7nY%LBE zN~3HTDudAgiw6$AgVEbZcMggOPc?GWQ;>la?Ct7uNz&@yaT7zC{zSbMjnl_w(%S=$ zx*MBLY#%k|dl;O3fRP|p6c|3S$VP&;LL0s)MP8({c`M&!yL=g#QhkV-GlL9|y2N{+ z2S{Y8M}iv!Ts`@R;Qj&fQ0LJfMPVs9fwdA0D1P{@^z!H+nY8O1=dD-cuby-Cet1A5 zrsqgXg;#uz`G0qv!}Fki@4DhkYzBYQm5hI!dd^|R>zdbCPI~^ZQLcxXXK_K5nApzg zOq2MY$1taslxS*3*quO94^sXqg;{BBLd6`nmMkr9=-)lQ^FPT|d09bM_MH4%KbNE? zo}jr~7a`UW%B44=@4PZLHg-BMn97{n#ipR{TmqBis%2?8uarLDN|-b6wWh6W6MD|1 zL>2&pTj=8>6_%eM9>wjEFOuw7dEG7h2b)Vj2eJx@5%}TTp%iO^E-|n4JRgkf> zQV>4iL+s9$>!_2L0VN)DrG1|mj^`wWB>8dII0d5X6N|e-drgvpKW4l%6&}hl-uYhp zC?hiAxN^2$WoTM~Mp8u!Xq+3r(YW6sWRi>V*jx{vye=)_Bpmkz53(3A2SAAGbW8h0qblCwUYQ|1f$)IgqUg2hdDFLeY#1o$n+hnl}h&DGUv>_ z>?YA{=~hRj!&Hy=n$wxvBy!=k83z~9`29d+8_`t`4`zL9PIc68b1w2%EZW(2%-A_%{`$R{0IuKl{j5WwXzKm51|;Y0Q-{OcT`yRVI=(#D3cWX!L!g z6&N8ca?PIZ`CCNeJSSdhxE_es&!OX)@hCSzmK~#5Z-{i_|W z4@MA-fgg;;_Lm6LJ=`KnCOeDE`aSRzeu9E4uh?AwF#m!uXI4h+ z_G@AdWm#Sb9laP>=B4kuef{xwKsLzn_6Xf4JSB%-chlL^UblZ`dgO++=Ts*RrXr2# z>IfH2BMC7MW8J^Fk&SOE?+AMY46u`lUrkX}^}536pMM}fQn1sJW+%Y=WmWCK&uYd^ zAmU3f2ip+pdD4E!XdPjE8(E?2&!NG4C-fy|HIy->H;a{B2$Z~IaGD6p+%=v)&!xuT%_Qm{oiGe;s; zbT)}|&qFWb3Axv>9nv05FWOzFs{tJq`rKafOaILZ;}z#i&=m-tNyEpImW_nu^6XAf zbV7|wfzNQI89qD5Fh-uqd!MjC5~YNAUEmG538hkvMksj^ww?z=%4b_NgYQSXADsFmIZAPX+%tuCw z7scY!AQq%s?q5oV5Qi~cpJbYj_*yWAxKI$-MDav zI#xTfzEXma`Dd0gwX20H@wJqw=qbPm(CIiO!Gc^j3MBf%CAcE+K%sFh;!etB^A2c+ zo7fCC^h8KPwd6jsx4IRYSnGc8ICfL-ga!CjEoQRF2vmNebql*{UnALZ0s_1K5Fe{A z7uyG0v@PZp%1U|vm?Tr& za4RgZPj$I-ftwTS1)Zu{PG>2+1I~qG*6SYqpZZo)E1YZlSwrJ|RFZhj%his}3Vhkd zHjUpsTxD%a2xMm~`Y?!4>l+HLRD-roo7V>3r=K8O&UW{z&lSJ^!ncnTt_A z_nH3!S<{6WDn;Jcm`hw9jFjJ1*cxDE;EVW3P`z&>v#_v$YOHZ)5~F#GBYXC%{YAi* zCl)b$!RsL}9umwFo4!l6_+gJ>Jet15)KVPIbB6a`WDfz5Y3Xo0V|F(%~ zs(K^cL^W+51c&&nXqy=IQ_}8Oz7#yflVHzU3$z_K3;y88B0kT7sJLcMiJhV<_DKpa9P;5d%FAQSj%%O@yTMxj4CDJ9 zg|lQt4h=>ZJJ9PYDd_kPhH3*pf+sL;{0J``>PAnN8gg9iAU}UsqW5(b!+U+#Pmzlp zQA*mmH9Y6~5$^TjL%PTw`+6U!!ZtLyif%Qc=4}ACm2DtV$yQ1|LVC{$6>ZGr5lOZ1 zKH*MdX5!8=1oU12!RX$0DhBRGZt}mz)x)r%@dSz|Y9s?}lORBrh&UEP_iVEczs0Uo z!Fv=IGP&b~G<1}C1acv}gB?v=oH|5a{&wet>u@}xoT>P1h>j5){tqLg>6SJMY z_2rqF?17RA{1ta|&i6b1gXkT^$NR5=Gq3yD>JbU26UWe`QahC zr!323_#Qwbh?(T(+_+I`Q6W)l+C^sF~Wf5Z#wKcyd$tZ_Y14)!J@Fk8is%K6nbB9JiVcK*p8>PH!Po zeT>{Wh{72*EZt1dDShbTH~T9sXq+|{d)rW%wC4Qr{*%KRXRa*adCvKPTPU z&bjw^_H04>zweb?N}XJk=1u0Ew2y=~uOKM_*d7LUO3ze1dKFQF#gEVSKYPfrL%M%M zZu%Rg$|ny==m*n2Axu^9h^9Vmh_yb%v;_Dq#sh{=%6N6cJNh5DLd@|iY1nq)pqawdA zJ2(i4WY#Xav~L;4JuJ`_?L8=TVb|CCd9F#4V@|SR1Y({h8envicC<-iCcT6RB!li9 z)JJyaxs=~-*@qlP%puLm25EgTAbj{5nBRNkYZ{?&?@8t!_kA1pxLs{nb7V9W%>>jB zf9PV)YZW_@f$_R_o8zR^jX?4AtI2O={jN?)@r_8`j$y z^;QPY`K6t)!CgK0BM0x*IzJz2elHP&2T%P6_?5|qC=`j)QXS#m)A#0Wo(JLFz*yVk zm@VgMQq3h})5yJ3-YLSO0wmpSg_-(=P$<1fRFu&M-OAsfmrK z9}BBN6GiJ!Ws(#&JR&>(Ac$of*b@XnP2YF>l~_t4Vx$icC*W`nS-SAAn_1mQ9oi0% z)bO+AP&~7T&xb%W@$wiv-;8|kp_i>-XtV+oZHsMb(#cF*YOI9vuFh`;PKE$I_oSw9 zP6}rzF@YeUBc;A~PWaG1W*XwU3r2uP&0m7>w01bMt!s&rJ?u|ME_cPPefPelQBY1> zy@Q01<*H7pgzFa!;W=n+Lq83Y!=vQjNtyXi$|Nwmb|a z+N(BOQdv^M!o>(RfayTCTMNi#dpH|M`=ZoA!80L`$Tl8m3Z`DaPRrwL$BOhwgwdJ|}Rrp?=T;C>dE9tbPWDUqm=n~;7_+{nNKrd&_ zu1B{x`t=8~fiu^tFUc1n<{M@}Zw>#1jFjr^SU0+R{Dcgr|L@rYdM!LURTkOZq68Pj zj^O}DiRo==zf;&R5dM2p`L%alOPVHYw0}wBH)(MB;>&;1X;g2!6!d8UAaQOT-8T1Dz8( zh6UX0Q$fGUr3a0)BqbXrBgm3x=hlDKq<#tfgWO)!U0ilM!V-#bDSQOPF^KlC&O~UD zQ6OGV=8;00HvmbKLl5lM)pKq&6Av6aN)yy?RXl7Ci2bAQ)^N8@SKa*daoj~vyC719fyE$Qp*2mxg5y=_ zP5w{s4E^lg&zeglU&FEP5aYfZIRmvbItKwO0H+oUkq48snji6n#4%mz#cxQ_ko?+H z0Gibik5r8rc8>UL`CY_F31ZyhpvR1{x&V{Pl?mDV9<^+?D{U9bWrr0KdsRu0K``rw zDJ*gHId-#O74FJOKG{j_(^E)5{SM#UBx{r}0i9hV}ydbAjN=^Pq z&0UDVpd7+M&R_PnbM7|t15m{u5G|5gqBpJR=38NKUg*r#(@JkC*R1#Gc7Is^dr!n* zJ@uwa`+t3*_j;BqdY z@R3QMiNI7ktNyFLrzC6iy?WvNIjdY&T8jE?-E1l53L!_}5kTzm0qe8teG!0?Wo|uz zs8_}Q_8l@pZBc;=r~KZzz-Q4-1rmgZr1;idSf=MW*~f*L3#nd8_ukX~D3`T8wS;-v z-nV+#sH^u?YKs9fpYR6tVN$E{bGlw|`6QCTiCrwPgouZv{`bXewuR|-Zdy-xmLQRa z4lh1LT$50+C63)1UW$En<#^SOD=mQ6rg3%Zna9X*lGUl`*U|=+Qg2S8k_o3CX1kik zc)6pq2D2qTb?Uh~wQ1+y(K9%!h$JasT*3gRGSxeGT zg%6EGn)s)fe>-8Z9@`_5JA{gBaZyp{eiw$j$CvQUi5Ltl_{Uxpo7N!@RpzdAO;XTz zfG$j^Qjcr>dyZG8A7kHbZdd&L|8S96N^xv-rnYyyUB~=3{`lNY?Lua~G`(SQZ_>WY za?;;y1*pmj_9%*dp-d(3DT_xC%n##CAweIfz1Kwy{GQkYO9dB*%~`06WP`VPa)oH9 zY1y$~B!qkOM zvR77C5+%tfsU$6xaE`sQXJreiM2boxLRyHVVJn(eMTzh8bKSqk@BZU{T-SYHSLvM3 z=ly=Yp3`@1?|)5g`!Z+ZR3^g{{q$7wAcTO6DAw$J(87AOl>>A@S)I#HkLnsv`Cp5@ zTpSTX)<)3}V`KI#0lZwoM}yEQ&)E(J_^R3Un=<4=7Kc_v8h5m`_+BuT!9ZTI_o4oE zRgOH^?TIC)na+yr|Es!Eg>G2zL^eIR^4wI}kc)d|8$w|3V!40!&Am!Qkw*dQ4PtFC z5pKGTi4xM*+|4JIJyYcCwWT#UJpJ%LY(D+nCii8)-nzpB^c7+EPsY0`+WE}M(>B)D z)|Qq=d$fE~*NK12VegqO|M&oTJCJWjI_?n85J1}>rdhl_TLQ;pP;1@I^4%vUjeF(w zkKE8b`-s2bUg#et*ZV?r>m&s3dPllTZqxL zM~<-YWUb%*3xcGq4=k{hj~Va|FgA8leY>P$j1fHUK-$NN8uua$x0C(R6qVQtzzG5w zmpbyBEGf|wHom^G(Y`w3QM@)v`i*s4?;5jevvbb03BG;;-x%{3K2;en6*3PI+e)Z@ zw4x3(SHXaS=xZPKb@&4 zk$oaY^|$UmrM7W9Wv&7i^CHineHM4#uP1vg8c_%$93Q!oiDdV8-z1W@mc61K)rEQ_ zx&`^_qW#uWuv;NOm-Tu9%MTG7_L3+Tqk81?b|`=oiDa zDe#QMDt%v}w0rGaiYSl%%DWmh@al7ax4+4Jv*f(~5b~KZrXOc{UoCd52|9owe3G$^ zo*)62KG~-nRug^PgxLXZDUe1o4kV=T!E0nv0p}s8=ipn;8vcWrl<>E-FEI%@?p2>| zRm9#CKHD{$spV)g$qUAOX0r6@zHYzLY@Q8!0!8;4=PZuP?>N53>))$~F65wNz~L~e zskim|Y1gNd=Ma7Ja%?7mLzz9Pm~==E%D2KFSH)@~?7!MQBKVu5ERCM+yS7rFQd7I(5RHmhJMOSmVA3ki>! zf}-Nwr%+_);De&JaU}%`9L?|0_Gmx`AN)(?BS$J2GJbH?zJP`{^<90t5ZB}X(Xy^3 zoB5fIZ5HtrU|}O_9wOKb<4`lm8Eh|1m#Luz=e@t3MgVX2Ntrjsblr@jtfX>z6RMArh8n5#Yy`3 z1)w`O=lF#rEUH-IO*iRpEoA26gYXb|)M~V02(KwJwRgNA>QJvFGD4(Wtb5}d)ReaC zC{w$^-WT4RY_^H8E*(UUPx=eE_+XLm9h-r5We)U;WMYv^ za??LD5zLqo)H!F7dIB#t0vOn3EvE_)YLYhVx>Q2)`j=+F>94Gn&wncjrZoUkHY_a9 z4(o{PpP3y;^*HBgSQ(LN>uEnTr}xeH68q$kF|%t--1~cOs&(&9L1;5Nx3iq!bp_U`otTA?%=M zB0ryP92T6}I^|ee>iJ`ilU<`J=5B(7-XD|#w1m2*M~)+8Bu{q{90_u_7UBxh3Z$Kj z>>id=CN~M0R2FuuJ@YX541cYeI#ao-QOp5!yl@Mv6x4Ixv2SyM4?qIbfk1jNsnL^wcDfob|5o?ofn zRV^v03{q3gF%RF*-F^SoN&lD4*5M>`@$3I(MZUbeh213L?K7#hrk2)Z`T3#LwcoN2 zrNmWc>=|sb_7n8z#4-XuIW3#Zoa)6xcT}Hot##NYDz$gZ?zp(upqb@p4)gKxkMf~o zX>$~PE$a+xcxLc&0C4i$pm?_9!!NvW-4qaD;i{5q@QH-~g1$|2wV3mIRG=d6~ ztn_Bqi8UPT!*RJ<2L)O}8v{jR)OM8KSr@c|aGXkqVz!^6^k3I@T}QVT>~QYezEcDW z3+-bzu=G%0oF14S*rW7TSV+QQX?%1)R5Z9MXmfUZksr7D@^|FSF_N}h#L6t*s z8wdx~nyBNbsgY#IN!?Md9+j1l`WXBI(1Psr^jC>;3qpnX_#r{< zs4;G4va1gy--q}NadY_3NIzq9P305HEK7{a*~`S#!2?r+S-@wH!OhQA9$8cTWzx- z5`tVp>r55U#=PFsEhA`DYM34$63NIk)Pd(MAQ!RKgMj#NWY8 zwPBuJBB%g=jg7I9($Fr@Yu1u!+m~%sT7&tK64BgO;dB){Gn~>q7sTV^bRDu}mH!~0 z4?8z@0DO_YTF&*eKLu!VmPc`Tg4a!?ABc0eOr6It2eh&sd(Hk^BP~J}Ll=f37mOM1 zZSECmI=$Zaq48xVV+N@gOBi!t%$2KcwYPpuHaQ!N%PochD3*!2@pS&VFrl@PinMjk zaX}8Gglghxz@X@zupaGVc&(vk`uhH^X3)0aEDM~40Xi6J_C&WZE`zj1EZ{Gh)N?*p zzONfED}VCt%KXllYi|#4Ag9k;3V$XNNuobA4m zJ>M6lo1Sbu|G1Y$$7?DY_fR_hTa9ps?WJ;lZIX-EWlQpMO!nf0-PhsXG#3U9= zMQ35Tyb8(p2Yq0fm}8D9G;1m;)1p>e>NrVf>d^%DvMldL64(0miFTKv^}~mR43P66 zhCJ3{cj5R7S=T~omOI2v{d(P~^xiuMP-zsTyBlpXcaJN&F}yya3YeuhYB@E>d}c^AM;BUFX67$6Y<3q8ORzA}kP`Ug!+nv{ zTcr&y|JWhBjcdRDLj^%?{VZr75js%#XYthn^GEsNv7?Sz2N4IIkzrvag>r~g9EtpCZ&9<(c}lf6jLR4*^9P^oO_w}q*LBFRa3eO9%;0x zyWw`0&k}fwVoJOa+0-b~v4i=%XOgn-gU^w^Asr{9>rOg>;cXPNO!w3Ym*jeuhA4$2 zODSG8yH)PhwU)f_Y%FzBMx71iV3Y_@lhdb8ZHUiIH?atYj2LhLv;1z^_u=*4B)<^X(rwV+%Cgc~!+c}XhZ;LD?Dm}N+S zRY9)9%7Fv{?}UA?`Sg$9btA+~K(_hR)}2i#I6T|Lx_&r&ZoKb$z{YN8%Z=JISA8oA z-otkUMobqE4SukVeHOC*o*Fse`7}X2Zk0GwVsphBL#hT>Yv^}IXH(LLhu2-)Cdot{ z1l~GOIf!rrpcjULflw^%Hx$!jQYsyo{8L z)+eo+_n9hAWUjMF9fuGAZanB#t%?i3Q}y&7U~F)b+S)K_njd(r5>XqN=qYgxJ-qKd zl9!OZAjNU&{&{$+ErOBN@6-ge2ebTj6#LY?HgE`NmQkpz*5!HOLsJ>EkPC!PMu0i3 zDD^0|jVlRf59agqMip%QUygkL_1z*oY$NYI*CK|JAX-O#J4i+g(qXmvZc}Oymto>AgF06~F$4&RZj`_uOuP zeMpldE1SB^Z>V-rK_LCvy3{eJk<9NI%omw|)xt2L;#PkX06opnkYwiT4F-xplQ1y_ zU7nm(MJVuuKG%RB5fc-KI=O`UKY;|eyXtQDH9)BAR)m)#9=DCCF{+u=HP=&^%SHgYvh2Q(g+N2030P2j&!eNGNOR~42Sa?uZnln zL!g0s1L4BXI;YPeIGlGiX2&FgGXn=uxQ2i$kR=IEMGk7^BA`xYz4Hd`rWiN-?ZT19 z$6jsQsE4$tY$Zz5eK*ymzqoV=r%4OFHxnz%VzAs-o8icSG0pvvttSjGAhxrdyTD0Z zSV$ab{}zCIhpHY3AL6_L6&XAcD{tc`1EtO6Cn>!}juk}pG77OHLT(PT8WB3FQfuve zEbcd0-CRVWu`GGN5OzQk3e>~j#Io6E`7pjihAYU78(Xa=PRByRUL63 z-WFq3%-yYjq~M8tdh%X|ua34XGvP4K)#D+*<~N&ja`OkMF$u5*sY(joqc|kt8b{O{ z8PI6fL%u_JyWnZvi`);d!{C~b5@s$z_JbVN@lYBNcc2WrQ(-SHu%}qv+!Vmc z)S!R00=<8rz!eqSznu~k5#tTYVcJ9DU>)vAk%f{; z63ljQ1ki?m8oIf`sS|Vtp4IWTW$V1pyMGs#9ziR(7tf0g8)Q1G;PfQU0=c_Y&Xt#K z%lf})Uwm%Cku_YOvL$iKu&mq^4djXw+4MVJi$9q!Ph)K6@$YW7HOb@JFE?5`~3y|>M@ z-OKo+V@Rh>`uBne364tS7~ShK5^mH8=Ouf6oHNPLE}9F5u=VCzg%k-mAMc>TZ{mtKO)vl5eI_Q z5~$(L)6X}{6!`Sa;iHagGwl!wJe zsY#|XOT~LD`v~hBwAkJWDXM#r>?4*)L`}Xbdx>pNQ12Wu(#)(Zywq#*CSA}!{|BP` zHU9NPg^XCD0+|PS_~-`Xcmy<($SctOJ7bT4!kZDvlA2G|^4WXv3TY#d)^^^) z?Ny>RO% zwK~H@EiX)o=&YCc(bQv>ByPkfC6xK*VmS;R*r_)tYnR)sU6l;y{s}nsrr%XOIBwLg zbatFOg#VQFt$_r4sr}+JB%L0tZrAW)f`PsNVLW*B%lPk1+8tx4uokb{x=rCa28z$c z?hf+EK{RHip!nR~ppQ8RBoH!Q+e|X$^|TmF0{Y>{3hcH4{?v*gQ#M>yC+B@<=%2_(!R%tZ6!8GqraEio* zvAM;=8+Jpi@qUv&tKxXsSI1=8%aeJ6X_Qf~Nb5(j5~{$lGzj;{X3$(5pwob51|=R= zB(vn{%uPwpP0z^)aYn?cZwt*W}lsLK3wL`yM%yn6e zf}oYc!*NQ>+_Az5ia$I*xB+1TL6$m)sMn1Y{l}+_zIAn85u+!y&s1g&vK--35o#l= zvWL<`7{8M`?$CBLlP?1FOAyt(X*;dP#0Z4y0i-3!1s%_Pi0_YbG}Q{;`tdJpZiRJ96a9H|d>n5MZ$Rc?h^?tj z;-WLFRr?G@{x2e;?tSoAe{y>ljK>C8YO&uNZrChP2y|iHy2|Os`Uzo+T@cvTpHfks zlIEZzrNyz7t70G3He@QCSHI)=^T$k2>+BSSZ&{TZ|CXhtU9y}n6*+J%U_AjnAYV4t z+Wey6!=WMIQLvntQusRrnZ^s;gGM4@hmMxfUO;W<^*#5A72SAov(<(;>gu58tu_|i zYq#Bu6$`huHZMMgpgowG?8Gk8nKVpbCL;DS>K#J63Y{+c8AKL-z5UY~rf1 zGzX2Gy~D=%7bnfUThsLlOd4^2VPa`>krOz!`EvF@$b3|H-+A`qH*PmAI$QD8bb*G* z@K0PbRZ2VvD3h9$GRqiT{2{62b*;%Jw0NpSc#RizqDl!Cdi*C5DB|3!od==3hjRmEEc7n!5Z5r z_3!IUt@2{diO!Woi+&=&=|k}OPZ{Htb~4;7_V2dLALYfO$zQ_fQ)gEohO6qYa=jJB zV#{LGl#qnG7nQC`;m>whN;iJqO?n)BXi$Cie^n8vu|hqc_*d6!`G!&fFj^9!VQ-QC zm9b5LUrdR=_Em!DNOqdiis7ghnMcAPMBF4g6ho48<+s<@*Icq04;2X7Lvg~vi`d(P zT?LF8!*mX$8{%|<{-)TmBKbo6$oW=2#-Ottdku}Q9-QBY7aYG2KHVa2D(5x;SI`P0 z*@);1#N7fjON20lUqr109JX7mZcB%{vzhchICq+F@5qc*s`sq{pb1RITP~exBey0C zXeSA=P9~#Y{?oIZ@MVtjnWz(w@JpdDYiqY~QoBjt*uuT0pS#rh#D*7yR)^h#Gr90% zq&#h|eJpQp8po;d&r5*Awq4Rm@%vcn+dc(lGq0G|BtYyn5W_xJPDF@!TPRBnvx=-R ziZ8D8vxOJp&;dG1S;b_a3`^FN5x5nIpEXbiPV;L|9vy0nkg;&jAWha$p7Qgs{nzhQ z<|Oe$Ftfhd;Vp#{>h9Q%r-~9B86O1Z1T-7Ha%ygo@24-~kG+!gqBg%VO=|ZEzp}fy z8}T96ovj6YRmAL?JGX8bK&+oeTfjFLgF<`6CGCJgfIDH~CF#sQhXEH)*PUJSYO2CN zdmt1;P5MhdRm>MfcUQ|OOlu0B%qx(A4j%f%=XrYXJgbL;Jqn_Y3lWTk=pB>0busPZ zdhCNIrDL_bOo0k~J$0X|5TFS2E0hV}AN+Q2%YMFp3j!aw!lh-eIaVM?f(SR)D|#9h z2$Bxcdy|RW7+x?Vm_#*iaNISn>Z@Tnc=ZEFG{SlOo0EWs3WWDM7lJgE-f+JOCdN z(t?#Tq~zgPhGYhTDL^OWc1Zn_H8netrQ%wH6q!ilW^0yL%z0Hh%5EZxL(jacSE&Ue z#Btln?2c+cJHDPY)@|RpCQJnMcuOOnYTuHgv(DKT+7q3~<(DGdxB^*M{tm6+SS5P} zOe+g_Sej7P-!ip3S#djX${XG58T!Mgbs(zpDc0aI#{=z zCeG7yi--g<8i?e*=3I%n4wYULP{yye@DyS86t>U22f~j~&)BSUYPTac=kOh) zSOJ9zNMM8COr!EnhFk>(5&|g9x+?MY?QQNt@x$RRpLWiKxHuNv@uN%W&OOugJsw#m z-SZVi9q@3Mv`x)CV-lVa8IMohY!|y-=hINMjwILcKxvY7UYop`d==1eFktie0~5a6 zN9;IFKFvW_ace*qzb{+?ICW&YU&#=|J*)AlFdsKMz@48zh9v*xW4?+KK`9ZIbiTWN zUw)!bI+YEv5kdgwmWu~iHfqo@X*20s}v!Z*zo-2#!)IaClin!I5b z2s&B**3j#JA)2l5cuGdyhd*dY)v^Z$@CBm&g!5#61JhwOY!0$?o+ZghV=7`?fZL^~ zj|bl6VQaztFaW=cUt5$2@X#EDA_w;R*jp6<$J)M~2+Zu`Hk$;xjbDq1vbeT0m72Dr zU`5D@D?+COT%4Geui@FC;cZ_rsz6b}%1VFwalcYI8~N^DrMEx0{%%T<_0)ex#{WBn z>RyzRC^?FryvO(cvqF1I}yUy~pxWGKt#PyH0eXlf4nF)+RDh0_C=Dv>_xq5^fJRqstS*v^O znCMvMkBexm_10*97`f>w*~C%gPN{AGmSWa&H|5u?oa&rA62$)&_S&&@f~mBt!B7lmp=$= zOQ71ZXTb8Yxx{6;gv-Vq$L2|Tct6m?5}&t(X3RET#k&VzDOlXzN6j*_yHd%Jt!BCd z;VA%)ht7UkpCH|H)t3^(F2BLMZ1l} z(W>~2FMX$u)r=-o6V9KZhXw+%Ql)H%h?msW)z!OZFKdp>O)}V(V4;LCWh8m8GI$7M zI9w}$nxv)c&0!zW1&v=)vN*5CwarrSMv<&QEMT%V5+tEUjW!KUP2dI}#xOL}82LNh zx#(e0J3ufZ6<}-S28&bD2hQOxWaSnfu5BK#-F2NUoYv3LL|yb1J7sV<3N35CT4=Kb zswj$py8l__XwG;_Aeb{zT^()QUJtQi1EliPFOLB;EAI2#nka_6NBstq6jFUN<$-F& z49)Zv;i~^|ad4{uS(;fZ^3kkqudXu*M zLx_^Y?0N)bmgjcxtz`fz3aHTHyLH(sw4P%TCs^$|vP#H8PV8n&uJ<0NAAK7F&Oo>n z4{o-|^~xo#7sL*x1l}GvjFt`fid)%79RKZBdaIAN;<4fKX#hIXJst4Dd8n(mA@3V- z4!-X2zf6|`9=J=!n!<4biljL!YS(Qk4k#x4ZWB)3UY)hUfia(pNe@|~E%+iJfPl%1 zm*gCH0%pvKz`N7A-Yx%4faqL(szl}~N=$~84Y7o)Hogy|aBkM!Dw6^)8NktaBA|gc zOJH;^$TRcT8BgUYOZFcdqXJ)AZ`N#!`4GN#xw>8AUA0#<4vJ_8T?7#j`U8T=M2Fkj z4ToKd4iHzF#`4O;%3&@s-V;T0%J}YRNqz5EbYAQx5GPVdY#|xvzP);-M=j97$+=$ zR?dn|{Q}TP)5nzoeF(OIlXHjFek%urL|;~PbKA@wa}H9=Xo<7Fn5HPs;G?pO?J2cr z1O)_9kiV(-!$P3{@=cFBW5XPrmZ@5>3L%??=h+#M`uZDj4xp@itqyBfhfjB~S`jTt zceVr9rGq+;0KPRsiW;xh$T7%BfFhU{7s}Z!aG2Jgpr8rjTB4{3uO}XXE(0#w} z2cr4)Yn;oj0Dtf}1S52tSS2*D)#OQ?NW2R8YxH}KF&1+AD=9vAk zTxaSN(k<*#**6;z;QbjoqwN&ACWp}rZlgG#AxzskQI+mB6900Y7CfN$VhUF@y?AP- ziLj2)M2;Z&06h+e5r3Oibkd~7JiL2^4)^AViY}rq4B=<4t7f6Ce~*tZ4%1s#)Ami5Qn< zjI$NHwRcHsw)riZqSnSc5J3;xFwf;;3wvKzn&6K<)RrOG%A5r zfNt!OR8$j>xWuU)<+$%UfhoZ~uPlp6Ly#AmKnfE(C?v^Q&Pb7HpqCV8*Vw& z{aRwx8v;%bKcookPk^@U2rj&F7}8BPIay`L1FbHLAc7yf{3+jV!`jaCn%Q&)AQw(hx08 zBuMC8wK1UwFi7IXeTTXmsJD`hjd%^So<1hSp^qRkjP_td>l`a0{Q2VJCNn2;=9Jv@ zpIBBO`FuQF#Jy_c6$zc?`>}b6siJAd`c1t2UMgF*oP&82F`55?N+BuU3k3`2Tod5hcRU`Dve0RwhWV0MrD)0_qh-d8Yk0w@lgZ@)X& zRe5k5hkBGco&nf_1DC_tQ#H$)WOmthC!Nvs>gyZ$PGLDIzp&Qn0$wSI^%kxbB+$bI z_JWq||I!?U{sk}_#-4ot%vBWh^z_u!0MlZ{{6e4==mNg~0jt{nTX%WBi@eWc7;OHM zZDt$xbp@^^LLuVIpQ-wK1wYAYh#`0+^_Nn%N?6YG5MNx|gi%Pt6N&&9c+Wvmoq%Ed zQ~eERj%WK$3zqw%e6kUGU3}N@fuWT&Q`|AqN0u%WP7k4nj-w3p;`2-lAtyF3Iveh! z4LO>&rWFymeAZLeTP`HC5+!$7XE8k(4{{zfB;CrGqbZM0rGyCdZfibdn3%uodeq)B zdkRi#IRPkxtbxbc7g@Tu0xmNQYZWotxPJ4h`?|8?t05V*-ze&ZOw%8~2@4R=yoslU zBcTfF%-5Wh3)e=LX!U(JcDCY+zy=P<7%cv&runa?AVkK;%9NJ#SgB>^oCUrdws7pJ zub?D|{XG+Z_CX`Wmk1go$w)!Rtku(ehHX)l)1)^N!RHWoBiw3jE}d)+lQHrE;PFA- z84gVPxInMHAywIT3&5JTfAPmN<)*-)I9FyIeI!>LAQgI{?O@CyQyG2-k`XT=Gzjp& zC>qkMUNWJ62dnF>oDmTIba+!tTtfD?ie#cZ6%W&8)IT0H8f(jvt3V1Q=n19pdp-mL zPg&Km`euU6K72;hA3!s_@vC4gKiGFwsAgZUIY+EWRsR;e5rj09cbAr|(GPa=nFVq+ zmuv5DLyeN4yfP>CNlmBPX9j;Q*8NP{q7-xgK_j&X=1(sN_yjXSoH}uL#HxliS?66fQ6JP?;Nh~NFt?WgX zY3DF0f~ehcut(16+~~r8QYp1vwG?^z$d7C51R|Z(H7vGn|0`PD4gHK${NvHN#c9O_ z2ek+DJKioKgO$=|Y3tLR(Sa%=qsHomWh z{qmc|YMS4?o~9yP?|@d`M~pML2Z)P0gejvbRW8To3ej}*V`v2fXUlg*VyYBcO$?Pn z`+#fVy2HC5e8PEVIAS-hK-Bo+m2ReEs>V6p;Oct`Io!qF9RkWVkM0k7JF+k_LbF)+ zuHvX*U!=m>2J7XwuDwA5EX^YU1))k_Hl)W&+pl;ZIN-IP%7Laz90Nu}7b*qR#TSiz zp)61*n>sk*(cHh_=1(DghPMpD0t+ffX&A~l(DB$>=+ph8631;3ovy=iyvY~OF)`=C zN@u^!#fzYHkSYp)g5$YJM`gdPi8V!lFrMU9xvp+MA!&oQnZy-oKY+3fuAO`)v}bw7 zh48!1z(!~`82R!OQp51s-+ z{JK=w%?eDPlVQ#T3B(Y3k;--;1S=O5%ZVvZkLXk_F&7F4WkW?D{&B;j);Erzy#GNi z7*-b6#C3%9X0vbZtXYHcnz7o>iq(?-cM)Rz{8P5e`Vg5-n?x0xistj7>1Y;gqr)G_ zcl$dib>zX%PyVNKoPjb<;|2$=xWBkXZ#?+*7JT&VDrU>WEfazGyF-8*nhynY)2Ri$ zMJwz@{T3T%28Mg|L-W2rExKX?a__DiL?F{NQN>tS(xX#ngu9G;^N+nh>Mdtqc%<3| z4HPa%h_IN+GZq)5y$GU(U)G%DnY`n~^qof15q~(XfiHIsdUU7qi>{6aYILinhHsvB zpo9Dum;Q#MgFMxuscjW57g)3(F(zkk4w{y_b{sVg%!mrBgH)nh@tmj#tD@i>k{4lL z^U)GB@|LpdfKdXuTQO%4t`dHnuBCLZ5MM|L)(XDGmPZJ8gA`Jf>KXCGdvxFjz}pl0 zKyhAN|6fQQVkeOrT~LGn>h=4H4+NXw#EH$8{x?kx#n?-Hp^lFc5Lg|Jm~3dqY2Vl& zJ8OsHZjb8%E20Bl!4e8}6IC720co&&0v5F1T+eS_gIRfi7NNTEtE2dzndC%PKGV3s|_=2Q13WQ5$3W zv3}&bq_f)=#GT|cjuC7XW@+};xck8$ISrAHNGd1z(>ytiK^U?lWZF#knpW`ICTJ_g z$vo0Z=alQI>hmMQ6fqy&{K~Hcpb|-|=3DH&IRppU{|2_x$QkKLo(b@^K)Blf#i$#1 z7w~Tsn1mF)tSfI~DI64fvIIQgAsUe^`sm}RuSc8!7DU!+nT58I0P?ojVFUCC!rX~g za$jvPNg@mi(QKk3TcomT3g((aU{vg;2Z8psxQ2g(as#FfY+y2r&NWAp?W3Q;mGTe5 zIA)Es@oGu51r0W;OrMKv+VcZd0s4D!6y(9aC48N~$rCAVU_Db>MhZT@WQdro_Kcr3 z30BYQJZ1lJ+dGk$M+%Qtm(S~c7Bh8N!T}EIj`iYmwh?CW_8-4@Ryg`#@e2G7LGY;@5iP0|c5<>cH&%Gs3$Wz4Qvdf&t+Gd+L$^y!Ni-{0OwgsO?-YFjS~3F)NWI2(=5maYGevk7gAIvM^& z1J#I z2xmmV+*8f`(k13@lMV%w%{MSC5j|Wu6atR#i?h4D8E}twoK$=fnS+JTrP;6KlhZSk zxIdfPx$TBE{cejnG3irUBpLU%T0s0ZR9L1byIaO?0%Bp;YJe zk++Ajc2Qt?Ps1=lcQUgp_Ym;MM?1e8BPGI}Q7IccgPgjzW%tSJil(tPx*> zdzEqFzWq1W^51b-#keB5yt_#8Iur9D208E6g!!}oaFspZugU2)Qm6@N1#2aKDe0?M zRtu-T`=%OkOi)rvp_|K7fBSr9|AwU4LsGw=lHUO-LlUy9)^hxveRQeGpMNl>;+2x` zmi_OWkY^rX%sf^KrxWV~_dADO@uUwkr=5_Cwb_5*Ksa||djrm^k@GVr@5l{1TphN) zAX+wIy+rlCx_H(@xL-0d;WEm4YF6Ygr%fEjD0{alw=H94#xcHEKV(U#=J$Vb&LADd zGZ=~p;bNF}Vmb0QDf;78&2!Us2U4e&h9GQGUM`+CK4qHq3i z9<>d%-7r%EH@&adUA%iX%T8_n?0G4^1(1MP_YRSd0P?%j4b=&*w&9r|`weG=`Wf1J zRxfe}WJ<9=KiwUX!P!@fEeH`u!Dz*R8Xl=kSTCt<_By+08lQA;r~cUkb-b)ItK!hu zOf7zHP;o*OsiId?@ZLnkl=m<$`w>#|bKkVG>Y zH$sttBwD^c(Pg%tvEc&YQ3m#Kqsz8i6@QR!;r4#vdZO6WQ4CXl=O2 zYJ@AfU`Qly@sr-}KMr55r3|(|=awu@T+D<_9w!r6ftQyywUAr%lbVvdeG%s8fj^9Z zUM9gAJERhnv3vqnIhEaauHo#4Fz06e^iou|dag|f!c77BKMEIB(jJw0BvpK+nf0Gl zuu4PFCYRZh`<5J-IUT!O+q-(E;HJlv&=u9ZcVtahH>K-E^e<}``M%xR3nwj93hoK* zQn^`+*_WoJ(CxNO;gxKXE`3&gx=E0HlX9fhMskQFm&)d8aLwh-HQYAguw~$)99mi4 zdi&ILj6N_**co?{=nRa9)+kB&u{^*RWKW4Wv5^hcR^JN+n&X%pNC!K~UQXHHe*eYT z7sV@S>;F*Ba*}Hq*DXQvV6+6nBviDktK;_uzg}O2(w+#Aeml!$&3aT=LqA4XK5-X$$cAnFFK)=I`Bs@kz02!*T&}6 z#&L1QOq(@Wd?gSFt-o*Vm}pH1<22F=bCRQyW0Kd1&&m9U-O%y|(;YB*tG_A&=HdsF2v-kyX?PRJ{YYrBv&WY0Cirr9>U z1-cN3ask5Jw>Sns({~&vWt$6#nn>eGAvcSaXFbC0k7lH~BGY);!jl(j2s1K!Xyk|^ z?d?1JJg(4%X z0{znXaTcR6Sz0g-y#@$wo7ZgJ*@uiTtZ$Gsw2WOf{8k*`^(ZV#dKnfnP{ql;4IX~Q zvHoT!l0DyZlK)}9g8m49T)|UvwSLEuUR!q#93Bzh1aSFSe^V79cLz8AWS?rYO@*8# z=zHcx*W&Ksd0?B{?etjJ%XeItcHP6XpYfM_QU9Z~_VbDVkeWi2*{oV(|M9yA$10Hm zfsx&%SH3Oe92RQu`d;5QiLcJEisXu?UuemyRd2JH%QXJ=d1lHKGOA&4># zzuxk0Xnw|Dj?^vuTSj(Nn981Nan6zq3t@kNslCd=cfd9IBW=O*m|!P4ctEaVsPH-Q znpW%Sn1BBsG0(KkZyC4=oJ zItqNX#QeJAw%>a{Hh1!?N2C^r2srrX_X8U_{|6gWM$7qYp3-}_l8QQs$(jC0Y77P? zd~E1nlOgj!V*oSn2c4mop6yXbo!QyRrzv-vQz%_LoR3vHn74>uK#uZ{_jd(HHrZd9 z>1Z6gutiwST^CuIct_;VasdC9E{-5pO)GxrI`BQfA(voPBAH<&&x3n(5tRx>(l=jC z+1T3dPoSNQ?Hthv2;*krJm#QHdgk_K70~s62Clv7mh%>S+fGVPtd>x7{U(;U9|E1} z2xeNC6bH>vN?ZFU=o1ShCLC}g+X;2QyBj!Nvv1dR=c<0M^DjUWj}pd&hqh(1 zJ;la_M3FPL#Ew@|PYs3_q}`b2{@_X~qBurtTBZ`qFdpl%v{?P2bxf=ce78EDJ-eiR z2q^(9mgN3;1s7c8oB@dX#HbAXjoJn)-u>FbU9nOmEO)+$3-w3Zc$)B^ViL zx@zBtJax)BOdW4k+EdaqGhdfG$S!LVsnu?wOut7eh06bNvV{{I2PLkfw)mI_EPU|K z6dDN$dL^=#A{((3(OERM1tpaWijlEx?b9ky;=vpA4w+L9y9z>IkiQJv`UE~Wd-7N+ zVKVM9j-+;Yryo#BpoiYV5{64;SGA`Z?gy+?MkS^P*7BK(f|^@+E|Kp{!~r&fJZmN>*&Kcy=-7iY)Kf0G+_&$5E~))>5-%lQ^3r8a}knQs9KXNnvuI zAMi6M{e=D$z(8Cw#7t|#==r05Y&BHLi^GA+&SJMp!sLxgA3_ekC!ioAf<>+O5I*12 z(D>5LV}4-kz}d++HEOzdtAo+)nFMNv)x=B?h?moHEBtH_Ljcl!Y;BM!$l>`k`=rF+H6vO4JB3#+m$#v36Rb)C6Zypszu@U5ecU<%U z2)6p&&uuHT2f=cC*ERvG#|)R%Pu)R_%T3=eyS($Te(t3eZJ6L=)nm2yM6%VE6O&!g z(w3Yt^4&=x5kZR3peEl+%sEf{@}|8=lcQS8fmH1vs~^e0&Q$NzWqpyne10@mafAQ+ zn2xm@s9jZ1)187w7Cgu1?y>*yu~OE>0fpjP!*3}VjWGTrr`3?QzuT$=^|i02ATOr^ zdqIqV>{{Bm+`7Pwqe|4oeMh7kFNE%++GoF?2!v$@35(T!Vfgq54zs;mmFbVlQV6Tj znwhu)Y|@F0%<^-n(y}l$oaDH!v3yHZT834yHs<{qoi^Ta*x#^aLt}D*Yu|en$RVVh(|bi98F8_NL-YRwSIIB&g%P?GVTJ=k zjM|?BS#?F;`Rw+gW2n>n-=3%b5vFk9_^3q?XXHkceBgdWHFXH8NJb|5{5m<&YO_Pi57 zqTN0a^HV7u2IMezm0h(K#og~gKmr{K&y9cgjV+wC?mE$mm)|E2PRy;?jNA+J&*kyb7t$7dP7oF zit?LKKHU#6(d9nhuft2m0P!vGu9I>fjLLh` zW24NDa(hbdh9d~s+~a6%JHIF_G^5HmNWJ*nS0~~g8z{rLQGyT#^+;&llb7i%yWq|) z+jj9aa&5<7!NMOEx%4EJ=>pjQ!Jx5KAkWbkHS zjc9%Ls*fY&Tj6g;nMXLn^3?Y+NoWmQTBZ^+Ha`A+WY?;@ z(!Ih7F$~4s*=cE53ohZ`lNqsD8>=n04F?Y10v)88!zU=$&8$Ey7Ns#2-u9&(wPi}Hwn`a|j}maaVt zah~`fz)sm`7}DPn$1O4p#2yHnWUW4=LE>`YrbHS$JUDyufje)095u@P^)*uGMIQ!? z)h~{~ctZsE`dDsnKrJX_EwTC@-9oU@=!N8sp@D%Kt43!j#qLtfCS;i$hnIA+Xnl+_glFztp@4ok>UoUL1**L~*t9CGTLiKljR? zn;6*C3(JK)>AX}Doga0S$FcBo%~ahozFQO9yC-0H5+2tPijDlrR|{>=7&F(n_S(tT{O)6y>x+GEw9)C{mZg%Ah+#81 z0dybZzWR#`ZKN-meG5P^Xp=|hkXQm}VqtSlXz!Dpoj+Ub5X1ymy8gjuKc#lmC-;3>dv&cuLt0%7jlZ z`7PQ=@M;q>?z9by9WjX2f>Y zIBB}=|Kv>4bFm16z=3!tfgX^bfdXc|hAHpYmwq)8m(fr?Q-!c17gRbP>IJ(wmrPXl z12urV68*dGxEB@eCNk^2zPxyK^C%Hch%aZ6?WEt2p zDifm(T(mhhv3U`df~}%P7NywY@b;X8g)L_Mnmt7ya~RT)`zyAy?>>PV5WzrYCeOvl z^VG8WA*9x06lsJ4C*9qAqnZ&Po_L9~lZEN&>88BIUhbW)ln#2-c79XG0`CZbpsBvf zeu6TA!s^GsEfkTnzdWe3hf)YzJ5DTM@W5f8b#y?Qto{XYsCsv%2JGDT4!DerOSh5+ z=wVzLVlMd&-J+rXtfHz}=Y*DYTAA}lE(gd3vlwM#y#<%yaF{Lkh7abZbUt zJopAVTK;IxF>yxPp~QF*j{}C2c&`9W!fFZ`wNV?Q%v!>&=5^C$f;3py-EKTY4ci*= zj?Zcg#s++b$OQd^ht4ucSA6xSj~^kq0__Dz2kSK{Q`X+%z>EIKo{LVZt2<9nNMNn| z;kDY@`I5p-q@k{2Yw6o|OzqtCEMLJMDc+$H%QVcFxaB@ifKmT?3eLX||3X4_#P1Mk z0;}iuw{?2U+4qi`KTbc++Bkc6#VI{)xBWjW!{|TbkL2h-HqAE;r3H<_!@BzmUJt5cau-zgKF__o6Db0WQnfRQ8AVjEP7r^ z5)Ys(X)EkC-Q&1W+?>|_uv|;wm)Z-KOHTmwNe@{}e8#=xQ!M%ym#b1kW4XNQKXCA_TavYeNbEQ!17VlxaQTNaN_7871^lJiw;nU=! zc~gjF%3!|Tx$IT!*Hv#Wm<*|rKpIA3=vj@C=!Lpkg1X+W0p`! zDx?fah%%KSzuCr-w(XsFw1wmO570rG~MLCl_erm z2xC+K7F11`zl+i4~GIPU6Y_WPivkm2DfPv2r!bq<=7(`FTGD3oPoZe|WU zdi%21$Iq$=!g=u-cnTb0h_E{gStK%-8N=!w_a%xHu3M_ZgwzcMn@)S<2ZorP<|O9x zN{Nrq$%IV^y2v617eWRi5)l!;-hOYBjDbQz#Um=C{1Zav|J*#-o#uw2nwM_v3;*R5 z(>yewhtVr%27FJ|iH{)alQ@I5qOXZw07JC{?TCsXM4Yz}Ju(9;FSgXHILfgg0Tsw6 zk;>jI99RT%0<1p9k92PP)zH^XbyIRS2l_!Z=&fRlUSsB?*mq=Dp#K49IyBs@V;+PC z;~HY5ppHbCrk&lFiI3Yi$R5})-HKah&|6$m5^x$oj9)Mre7K1($RX4@oK--!xmEH^ zS(QNm`Ke8Hotr^v%*=CjpWA0@shuzDRh!TKSZBp~{lGGU{BD~{M)YWnvD2-Cgo&W) z;g78^%we`VIv1T0W3e{cU^;zJVZqMRG2B#@(*DYfS#-Y3C9c#=Tbw0v!g;eEvSgxFzD9`G? zcq#FV_z$d|QXwkRP%RS}*=$=R>I!#bph6&y_9vI3#$V7Q^FzLlK^I#XWHc|(vbP?W zBSZh$WT3~SYj%9+@-cOmqXU*Vbj_7gjrsWarYr_NWN%@6Q!SM@U2?_TKX+biee7E7 zSjdtJ+og($6;z5;vQgV2FGfMe&s zoOd{NYA5Fd1Nyw&Tw=ErAwSv3u%3AqZ7ZH$&&Npbyrs)2{AS0#UprN+exDZWv|M1F zQEvP#yUnz*Klh(uwD9*;H82in0brT6{Uq5DtWIJyt6tB+XY{wJeQ&OE+n*nxQzv+ZU^`i8Tp`y;P!NycpUty-tQKBE+iI3&wh>9o4R z2}i6tPu2_-N*~Y@t5*kn&ptKG2u#|OsyL!P^4YH! zUur~azQIB&mb(*<)3jQWcASszY`I3huae20A*v$Ah=(V-c3MVkeppXt33G&z8J}>F zxw$!Nl?JxMVoNa9Ba2NfZiN!)j-vx(G{=u_W#@D9AlAxy`aDWiLpR+ftyJ&BHUuz_U(P{#n1P0^fv*u8^I~ zCivG!Fy~wsT1VNspw?rj*Rpr|m1b>d$k23Uu18E3fNdhJ+2H#f!8_H}%Y?ymEGMaS znEhV-vhH^#`;4w(f%7&WzulECzjgelNq79p%4t6)hQgw>T#IgGon14{!xVGW!eR!g zVsbRI#W1QydC2`IyyeGvtrs;0y9T&*>qj<|^~4@|q(NsxXnQmLzecm{tQ_EtWie20 zIH;8}44|9vydoh!#dYv#=|}+f{NfXGq`E4kR##V_Q@fh|Xe7XW&8glB3K_`thtnZT zy?D|F-no(JZbo9e*mFby&4E2pht#$)I4gq0si1aCO!6dHVj|z=tsE=eaYkvgzdXNGQlwv(`S1I>6udb>YWw#6L2quA2#@_4$Xe5I+Ok<= z=H(VbEh^3_RhHyx)A>kqW$Yks#rj=uO6{xEL<5=4{q9H^TBr+8!nI}uKrmy3qB(+A zBwm`q%BrL5iBaI7wBpqxt~XorHq3GkJ0unM2Rd?EKFtW5JoZFjZx?f}hu&t6-(QQZ zON4`!?uYB{C5`P}zEGO!@X1lL_FCw6*;HeOpJ6zJ{?kpo#KJb{5s1P!%fuz&=%}(Q zHEN|go@e=!JKiW_iC&(s9I#vcVD0caRWZtSV~O^GfMvmrjaQnS2%X69*iHQf1^kf- zrg_d~-H>(=;A?5-X0eF(jE^?le6Ir5brtd+T z?eM+5`@M!i)RPVVr|*loXqIdnc9yoZ!mSB?nQ(b|?ZOv!;hJsn#tV-QT3O8_zVgP6 zs8@rUtBdV?ipLjf;$!IzGUg#b1^>L;VxPLh3tn1Y+%&!sv^AwEp5>GXnA=V%3@^z0O&uPh0fc;EfCR~@;yuH zXbSumc3r?DFbh*O|ER@HuKj*b5M3Bi8tJrfHX3=*3bX@5hpKfTFZ}S&?tsu1L*Ru~ z=siOIj!Dwd@Lkb=C_8czO&RJKkyb7;$F()$8h_~AnxwS*ctaqTVd+d7Qt5Di?<3fk z`Eh;)qPR~LUur`_FoorF`_kW7gTVzO` z!9w2#=#rfrPWoRemM6hHYEwy2L?-P=U!i6$+q+lvvY|Vu1qX@N?DEjT%?bOsUiho; z#SIFL@_?FxSOiHg@RFg4BEBt%a_xLSSmCoal1PfyX{hiLe?ejzdUO&U9NrEzH_Fa3 z2?mT|m(GlCv3qd-((9_A8^`AiSqqJ`s*2kLe$1R$?%6N1z^hulb(($p-a_?#ysdr= zZjkUuMeDH0TUo9C?W%=jSExkb$31h-mhF=BrKV z0?YsXf_Zu(y`5#)#$h7$#Y$y!c@c8bXJ*b=(uB>WuFhUu1s2^0e~au@+7qJjhTP`W zf#!#jFDzz98qHkecmCj3GTEy*04=_(EGjSLoRgRkX)Ac~eVlbESpw2=6n@JoATTT}EF40MHStGBt?dKokG+0) zXnwib?q2z&Rqts7!^Er(cZnpe5RYX)zDSO_q`ilydNxqy`{xg;4qwM~bmzsCuE z+`UBBb;Rcqal>4Xoenk2aeIED2&`8#DQz99&5{DOifPxY`(P|d+f%T0O%nf)g2Lh| zj7b>kH?)-}wusLpshAJ>G=apq<8c&3phkP){rmfh*Sm#M<-hG-uy1{S@K?`2MduA~ zrhj=&ewNKN2rW|1GU-&BmhsOKclopNP4S*D{cRRo$D+EKIC$SAdH)o70)qGN$783( zcv_;~0+N~};4^E_iE{w@-U%Han~>VT&2X=g$~Dbg zPpyh?T0Xt%;YBsljNAP0>ujx!iVlc(+*=}D+vGe1zbut$97)W<&}DK{jsm|a*6(sn zT@+b*4t~4`g9!fbe+P?pcl+NQy;=3iqK9wPutSfd)|EHhCS_RV4RK3BOC=C#UK@Pz z(Yve7L+-JjHk;0z>SpHIq-$HgC%%u+FR34|DOIHF^3O zBYct5m~sdQOyI;1I=-qI`~OsFMX|lH96wFafMfQBwvW= z&Z^NYWcpIt{Z5_2W|xeS*Ei*}i;7;&uMo{CqT6k-BV(_1imQ@Al4}4kYjmftTofpp zV%m)P(NgP;Js)kybBaX|+ht^2s6TriZm>B-$Es2W9~$!FNOXgbI!zl5#A!E&R4TlN z)Gs{=Z24DSd$a441H*z@SG&H?SBAOsHYu064p~`YN2uG6)009OCpGt&2CNmQ{Xi8( zl0hH%^$M@qSx5DWRJV-!fAX}O#p5cs)vELK*w$EkP9^EybJEhPhvx_&q-pi{MDeXj zEr04eA&S ztC1_|C_WNdnh`m9rW2FKs>T8nMPcQM|VTJ2pCQG1I@gZ|M?6j-@%t)5tYf3JR{w!D5ObqZ;q^-_gO( z?(cF8(bm)S6nZNHwrp)>6k@8G%--=-DyG6qAd+ZgM0FdtFG9-EKKf(5s=EqxV1|!Y zr$B=X^tkQj7s|>?dKXY01Lh8r=mh!9SWIp$pHNs<9hm+3ZtOQyC%$&gIZC$8=LAR4 z!OUOb*n*9*83y8JGOx*vO$pnJE3+WMw6xY{6x*{{>k?`%5OyR7iCd?ZVn zO2eIzSqRB|F;ko$d$)DMZ9k)!GVcnHxH6%82B-{1RU{+`gK)x{Xua$#sl%3*qRWT4 zhY2SILDR6X$TP3ICD+#4isZot>%FJBwl*_H?`V_Wx$}F#!1|!S|E4@WgByOMU=jhV zDrdj?CN3^65*vXqj%k+|ON2|P1)V)pDQo_v@u17}r-FhXRi}`6b>_?&?~0g1-N-vY zwzGv^oS^u7xreR0_wLNHNl^yvy>}~{;&a8+R~YhRE;dv@6F$mh@%xyw>%5tz70*_6 zEfO{%G|I8=-AL}*E7PX^#BE~fb$dH{{tzHpjlrA|Bny_eBH9)#SSIZZzX7!jCZ%2Q zg5WT?k402=D7`ii4-+ed%j)|n1W^yTS4{d}EW@9Q(~0G`dJE}diN zVBFQ!rGMl8uq)NBsIu0iEm6E_$Va@<*A70`d1400e!-tvSl}tn8SuM9U!HY+$uU#Y zI9Ho|%NQ%#@xhgo;}Xl>(+|I&|5)O%N>zL_z^fG|Dz$AJLB9SgzDTm5zaS__^A3%d z&y|YF^+1z1)>V=zZWu|)>GXKCfr+E=zi#xmOHrwJpM&bcH(UddG>*MHPseg^HT>Tvk&QN+c{icz4IV--_ydJL*W)JZEAr5@ zFG2kdupz82x34-LrppxI`1p76y@|~J`{|Q0UtMT7%~VAyi;7rzI-=+nMcy|&uzfl+ z8M69pysCH{qmsR}DjGt-^psQLA2035cQ1EJJ6>s5J}z$3Y25kkQ0|lf#&slgc5tyQ z256BWCMJDVuP4WlhfyL$(KQ!mj&-WW=?j;eRRDj;^0&C8EQEx&ib6 z8m4^Y&~)CHS|-ybdHdWuD7+A&<+DvlDB6tsbu;6`R>}i?k*^|_ve^SWHpPB`g+Ws@ zGqd39+MslqnMe1pGQm4$7YXWyuHkC`xveH-ZLk<0(W%))|y{8PnL zX48IECDX}sc2B&X<>Y)L1_LMUug27mu zn!kwRM3uMy#dgjkQINB@Vs>`+bmbt!$?1XYc%uUc90G=UCa8`!8_NuKgw?JiuDyPt z#*jFK?=lAZqKYoP?bo(=W&&XQ&Yb}`EL;H^UeYKo9?$AJ0Q0FhCzyo=LgQhoxGB~! z@w(AHEKrd863?+XHCV@Z!aV0{Z;MNLWo2b`_2+UK@n8^@@-)HbdjC98#u`}J5FuNZ zl@%tRTi}U-b0b(a`#|vYV&qM_36sohOU=GTXYHPV2S7`BVz;w#Vh+B&vdn|~RO^=A zwC5se8)_!m$#QovkwsE&=C&*`LL{;^*(fzPx^asgW1;W0$)6tzdE>s3-7ar;&$s+> znb)2%Fcj~s&1{Xar-l{@(#)mbV~219T&Htp&CT^t;gFG)4S{(M_QiYS7kqrmCK0mI zRdWA6V;LK%Y~yKWAr}fYHP^<%B1~QGM=Hm&terjLE{wC170)VLd_(HpBnCnJ=|yanfm7S#>JG z^M-17xly!#ZWRLai+~bFlP^hpqm*PC2<&(_?Ryl zJ}|e;Z(YF!fK;&=fFqr#gHRx_U=(-QyV-!d$yj0*_2nYaD~Jym!z`t@8d`{Rs;X@- z;cOY$FCLt5b2woK^If|xU15B0WC~|u?@M+?nv0EnnNt(dWFdfS%k_r!Cwt?XoIptv z!~O-CmU8M!`%AHi6-D)r0U0+kGebUkr6>?+QTG7}+*FaKR}3FaYu{VZaD=E9z!42fIAT3T zY}S=2PV>xyg6`daaLE#>7Y0$A4a1UHyL^52hBABFhhOM+z2EcT@O+%hlj7MP0F+dJj&*YHF-76E2rI#Bh_O&Bk+^lvJf>ejOYKnl3sI`$tCK zZIt`x34n)C5L;QLAG>qGj6}!EtVhTB@$1IMeN$W76gFqMDA*i6oMJ2?)GW{1==-I1 zc~};58a)f%?fTY(Xy(5HZhbCpct1@-IjNukr9hcA+$wHQxmO^*s!Fzo@+yis!%abP zo!UwQF2J)7L$`k??(s{ut%H6N1`THjiakB!LQV{$IJWGaUqc_BR&sEB=jwcTuq z$gQX6`E9z{YYf?cdKz~=@`CsXy)E)FmTFc()73(OhQWd=-#%M96B84I%NSlv9{G?; zu8fy!2YB-VM&ZB@di04hBGWYN;wPEyYuEzIaz5_65*vH5!V81gZ6rLX+yt`EIEy+% z^cPfl+aR&W*xdf|=Mz{>Wo6lu+f|(Iut`nOa}qb}TDkl~um|l$00QlZuC6XLk;K}0 zi54mU!&b`4Mq*FZpfbRSqL5&K%fsfd5<=`3Yci9P!qdH_N?mz}Vo6Ndjc4(ry1GpS zYK%jk+v%asdtF13hTZPV@a*blo9C>P@+)W&Y;I|5Lx(M7;n)4-DHeN7dc0lW({OWU_dUX?!GYgWFn?tvPJaAoHJEsOd-lg$4_cx9WbW9eOu8Vw;ety#lD* zF_BKY_3g-TmUqIFaGHA(7Ipo@)=fP(*>h!j!?&`eAU~a~aes>Ny!KTCEz z*%?E^;KPu`bK92(resf_+5fV>PK-^TT~GvtHF`aN9L`4nd?unqV{Ui~E!Q`g4xnjN zjm!2(!^bppJ2O*cZXJ_YPRiXiH{ZJ=crOIlB!n9uA;vZkZ|FR6U*1$ADJ>V^@-RUo zmpO0nd?mN%MxIMgy{pf>+vMJNfgz748mz?&44%{? z*D{*Q{yAH5jEgsJp@kpbwb5t{blA|^>G?elh5SPMj=Y*hFp7NqC&vRCk>$Rh0J~<3hz$Xx# z8OX-sSiO;U7#gdg!5TaFNsjd!Pr|ya?tGed&2Gjp{5UT+_tf~7>P4Gw7V*cNyRIqH z-Wss)U=I@|z8#J|VmwrHZ=g*^-Q!!M7_48o*z^JA0~!n(Or!gyd}$t=3)%>45=>4~F=3)Tv+Cb0%`;*ehQe2Cs$;E^@5WVL z-72OS{(fUx5PLU&Wl|-pZ@u!mjw+9v0bN;G*t-6CWs}Ax-rfo# z&*znZ8`bs6>+ItVcZW^~?CpQ4Wb!_mPV1V{z2k4v35)#l)`rX+;@7e*M3BzhM+t4F zYxY4W2s-D&{4b%{CT~g4bw2i^DyGuO7trM4T64BZ@#?KAj?Hqn*~B1M?zv+}g$zqS z|FeA}$4k<@})McSy z&h7w;4M0vR#)%y1)mt-(Y>Z9Ani06fG6d(=F{+j$0&-D$9)WQSI9*z^Zp}Vs29eVB zkk*4IN=Z&8it;Fp%243H=ATB+WX46Wy-Q$5yBDM71U^LjQws`WvLN3XCL-#H;A%Q4 zC!H}F=OR36MAL!c)vGr*q4qJ`!5Nfr=32+1Glw^N4)8fwYDEjpY!ArT4M_))u2}1P z|GJ;uyu<2WYY;3887zM<7K(YR>+fc6D;ET~#Pe8l+J70;Z~E@j88U4ZUd}Hr%4~uX zYd;hN-7Lg_99g`O=o}}?Xl?7Gi^%X2zBzUF{StIqr847yvH#Q7&pByxT0H)6Br(l2 z?_z(vUUsz;-%sP#wl*s(Xia-jd#%jPL)i?`^ekJMUx%NkW~zY&eMe3}#$)cCE+}<~ zA3JPA&b)Q&mPQE6ZYfc*+C;Bj)IUIw@~dxs?Hc9!AOeCW;B)l9Bb1Wsaw(5}7(}FF z!m6LU+JJXPPZ_6|z09BLk%pO_(`yYnrpA41w`;Mqg8S}s)`Fe_-pWfO?`Z(k*HmPuFqJzfssq6 z0hkDO4C_G-4mOr-pW z&6{GU86>IlbXDatyu7@c5Rw|a5l?DC)fnTYmJuNdz~3Y@{5NM_Kg(TZmGmQ7Xc*lo zf_yiN1);5+)n21F=3rrB_D%r{pl#3A&oSE`BNFP(IlOE(ZKiw*5mu|Yb>sA_`rBY# z0x{Z_@sTeI9?E|YSX$pp6m#qp=A}3$L_?(Q;0kd)L3k(ZhdN&Dp>ZgaudT+`dF_rDFtDxIDn?nzt7jy%%fr-)vwxc(JZ^fB}%)U+&<*)}EI4z}}PI7+KlPr47 zK{Fb%9>wMxfblR^Lm;xFRwb-oN{>%jMo$Zi$-()B z?Q9T^V45}%Coyb!4S*SaN?Y4AT=ue+1M7@+8~lon$Q;!pnq9+g(Q`)nWfY*L^hQ^4KZHd%r4&;)n=C>uLA<{-faiIIH7^ zP>SH63U%c*p41WvDHkk-UNehn8U2Ck`JzY@cmNp z^PJl4Tepsxo6AEbhS4I6pU=lbI$5o5fz+v#@KAR}@ib*?Te-L)p|^n)8y8>972adD z-VN9Sge$8TDveGh^V?E}iIYpXI-q)>$_J6>PUx}O=;~e`QWR_CpVdBYN8l?cZCmPGj zG+I&&0`~}tcTq#!)?N2_?;JiMVnOHE&GZtqY+oA~h;TmDC=C&%)<$@c1a1d%G?1N< zR6OtmfDKurZnZzWoth4NCO=Ehv+s={%VT*ymHZIVyP*2&!d;`luXR80|UbL zfh7F8Gj_ZeO>hq0XbCkx#YnngBmcLmQ{tRuUIJ?s_X_3d<7?^G=bvlSW7{ab&g^EZ zWO-UH-&~c!mluI=va|be!oAWYa%(8oSd&ahXo4l#v)eTC?H2+xkcEL*;VZ@C)G}iU zXEH4jo7t*Q;_FLaL;{ zlYg8kOf=a@sPcE(d*h&Rkpc_z{ws=h+66-FCwvl$QG`Ko>$ozrxh9HV1Q&$&>TV-4 zcOl-lT!v6OgWN8qkfU|h`R<2(hcj9UVH^Nq6~{U5t~j0D@1{Q@8=|OTXx0U1y2>Nr)oAmlL8NoWtbn*QdBl zt{Z3T`4HbSZn03_iuv2!tW<9KcTEd%l-ihxYg2b0YOpa@zIjLJAiO@^iGfCF$lwnI zSrFbv$5WiC(-}v&8CLl#{aPIMQA_qKlL|CTCBx+BqBNHWQBo6K`PQc{{yEgCgco!X zN9pp4C0KpayQ*~&mcI4$yst7e1is|GeZU*QNjoBuG&ttaC}-9Y^Dv!IL!elp+)?D- z1fJ~OMSyHr>~Idn7or)8+{bm2xfvs0hy`MO{##;r=ATHX^Gx?r<FKbH zwlYoIxMU?B5yATWW4V4pY!-sfU!W62v**T6hc)BhvABquEjY>Xx|Z$FmA&dqK^uJ} zci#OE^c9p-k(4W0-FXKy@jOU^>SGl1-FSN|mj&`rTU``_loDHZ1BK%;Xmbo41|2Omcv&-HM! zHz(2EcR9>G41Sd#!9)Eu7qNh26%sqAOgU#6&gdg4P#4o_EsSQzLmZ5;Z_-KWuH1;Ek*(P=*WMT1MaR@TX?aUSyFG=n0w4M!_XkFlYn+MHs0BI zFM?Dj7RDPd`uXSo;BW%q+{|sOu!KJ&vs$4DeDTfZ@#(-v&E4~>rqeZX=x6ib0 zGq{O1ExpIFa0=;Q!odrJ5TbpvhmG(al5P)kH^CPltz+i(^2u17h~_^CEOdft{K#LB+xM+*pn+f zQmp@!%h|iKq?{_$X=ACm0c|^;5KC$n%sQ29Ep>+)5zliP96v=ilcfDa#%L1qhC?u)S4!#Fb@@32$~t)0(^spiPWD~DSSPmhsJ4x+C%L`4cD<(`P5`kD zC|hW^(N#JvIo2(V>psmNU=-8m$5VheVLPm1s>DCcbzY-=54ueF#C&#zZ+v16=_Q&a zxPq=I9!NKFIy;F0khrV~V&>XMr3WDEM-qz13kv?`X|8y{16A2v;gyl4TGuf5%eaxe zKT06zMEfl0zXL`_?!_`<5d{4vo!sEVwBswEbCy(w??;>aeuuf~Uo*mZgKJIx-_;w} z-ZBh0M9&KkA#SGASe)~QogYCegcq_?(FUeeV6d0<=BauJcY2-LE_xiIzZ`GpK%lxw#__X#Hh3e{HR9C^<h#g3 zrW$YNAECdxR1}*l&Mwx>4=g}+L{O0QR=a&x-L4C%tWDBU&!;zZ(kq;r%-+iGbg{oZ z&Y#b@`{&oHQy4V2bd@*&d>RSCQhNW#=0R~UPQz=AWVk6bwNWeFOk&?|?mK&lDZc>9 z1dR2K%7KJif`@JprEGOXbJZj5IP7myQd1W-Z=0eR{PwFEj1(Yjs_sfZR3N~l%Ilij zb`3l4pwm)8w?{aWK#Kl+c#wxZncX9w@hQF5zT_>5&lgJIe@;rm{IIZ>lJZ#c8tIJ8 z5++PSpVs0MCVE9@^VqqgqzF)j*f%D?^VI!tyi{DAz*6{7fvZhb@hmU1wea+sPN6!DrZv)eyW3_({>7TF(f}%QMG&D;AF*?eeuo)Y=OJ znN5w{CjIj}+O#RXb3c3eoNuxgdGN<1Sy>Ga#SuzE#B~0P;4;0RxOE{}P9XZm?pgb6 zEfS*tF$AK7(S4(qt&iuIh!EV9rsVPN!Li+TU3|{qea;+1NHL7kZ_O?_;hD2-xHT) zAnx6l`m=QWTA2p6q(8KJQ8)T0<~N&Y0sGRutq~FI*26m?Ng)Q#t)EZWY%SUW|nC zP14+~Xv8&V7Q3FUIR$7nDUK4Fvq=Psj;`VT{KZB&xvt#_1_p`epq?Ot#4<+UecRIT zTV0m_YL=gxaC|L>)!kptaUCtiJ%`B>&ZHklr4M$vJP{Z^7SiFwza=a{$(~-he)G2@ z;e~+&ki@KKY1Nb<&KQRKWYa@W9Qb2ecEeBCQ};9&_$@QljpurN0+&)!`89@c5~A8| zxv?mmnx38x6~Jxu66Y2m@z0J|Ks#h*X68s{NHY-I9`=^#MTO*@=rD2*yDR<0?Xvpo zd@a6^J#TzVUD>$86;r8`OJAOyuf@lXkuUC(N4fxNY694Y=DyOdW8o7IckDVn z6W?W&G7Rh(ixP+~3p$H^yi7TrK7yy-<+Q*AK`fdn&DkxSS3!}kXXkINuX2N&5;1IQkY#m zith*V%FMJu(0aLbM=krOI~Cf>v2m0{hp3Suc9tbqiHHbDncB*o%8mf*!L`4=t$e(S z9Q_E7EOwd=0&$>Sain!8R35c1oF?CNP}8}q@>im@JRKwe)`Y;Owd=O2^KRx{49?q) z@bl<2TM?i3zw^VpHH5Jz3gp?FOihsh&fog3$%o|2QgYKJ+^ZYh0y5ObWeuNqEJbzsFg+Jb!dl)l% z4MgSOVjF^mQC5tzHBUxN?59w~oMnXLo%>`9CuXC`DtL{J4_$5IhD3ig(VhYkCPGJc z@8%60@jSXF-9TKfcN_oJ8T*o+j?;b$sa3U{7LwB@Bn(OJDE%lzc&u29q?py5r^lU^ zWd3UG8cdY3-!8bg)iBYu$49Ws*Vk?po?cuwwJje zxuOmP_6RZWE}7xUBu>BWQ_dT6e9n*_@8cLz%C|J?^6gZzvU)622nH73(DMK#IfQTJ z=1Uow6qeTci+90$^SY%a!Ie)SG9cXj6Q}V@d2ruglK+7~U4`YKQJUpKxG2(Nn}A^< z>S4U<)JF9ym#@_F2gNhRt-fSf9b^w7Zbo3CO=R4~vlm5f1=DK*GutLD{kJpoANQQ1 zXauQ&43nKph??JoJDK5)KS?CZ?5&R=-BqgmMD__-=)vR8mV~Sn0yzH9ZKYGITpGY? z@vOq@MshMyKlERp=a_At+S6z+E0*(@LX++Lj_O?l;__{M^I{Oiy8PS^I^b0i*FeUe5||N!0Iy|? zq~_*(9ED*&BXw-)A1Nc@d%+W-^BDJf1+X>YO94;?)3rcG=jd(|g~xwIJXvdM#HGx~rJ`3PVU_N=Z!fQ{e@sBEOqMZRE{u@Y; z)Q%mxhal?SDeEL>4{=5FRRP{;9l)Q2)RHt4;?6E7c5-=PdU+1_U-Rj9=xm|2B@Cm4 z*^aN){d+X4H@ra@8;`uI_$kU5)GxbzJ8eiyKpHQ&f4APOyj1&kP$?kDCvcBP=Kg6jwDiZ>d=hXr(^5 zWdwYO+3+)x{G4SN*9p$~JYS=%4B{~jX#|AYhE&de{ioQsePNsMWb3oYf(M-c7uJp02-vo3DLlU5yrRb>VN3>sZgpTqK zOV>=Ehxl?@Kq8Y~%dvM3-`zR8gL$3R`-STzZlKrNNR}JeUAwrkN+UMw{*x`^d+`1PU6_XBedr`jUPcH+EX9!d=O>V9IU^A`t$2L_1| zdyhPp3fLTzMIi&;PB4hLF)Nlhl5OTKkZ=}cbA;gD>iUE~gb5aK90to$o^|&+aFNd8 zzckNphvR;H4G^x{RoYslQl&odXYd9#OS`YbgOq?p$U7eOvIunvg;!3Pmlsm zBG_73+0&9>WdHBw$_i*)P;xf5`heaLD_=jA%LKh7x4nM-+C`yel8~rFQ7?s0xbD6# zpQBBRJQLG;bt5Aq()w(jYgV+ng=^~^wSO@5&vSfrYV;)0ksae+O#vNA`m(d|SN%?& zSC97ZraR}o@6f(3Uz5&s1y|yJCN$E<2DBG@l-OQanVNL&#AO3OiZ(P|nn}U|4c895 z#H8v%JkW_-4+Ql;kzv^{du;bXqP`m}BoQwP)B$ zd64)kWM?bCbwga+$Pzgp2LlC33ru09c+zm#Pr+`n0Rz6cp&EvGfI*7tq7z$1I9~qE zdY@XWkm5?IBQ{GBu#XRz&=}&=?cF6Dzn2n|HCok&n+XL6Ssx!O4S-?i9KIuO5FuC& zYPzcU86;4`8=WePM*;|g5dMGvAb*7xLa-;flsC}qWbd1dbus7{aLZtBWL+G17V@wp z&0PEDwWCqBZi`nT>rDs3f%dnVV;{F}Zw20~-O;1Ci+z}ierYLTPU%>npThYw@__{YIX| zi1&s)OZ=pN9%*v~E{0C9NrLfr{X5?kTz)?>{}x~Q08y(rXSBciuccsxR6LYOrCFKH zTqUv&B{-RKnt62PO%(qdG_ZHGeiEYJPY8mBEI6_>b1lC5MlHAE2*F(Fq_Oc~Y}((} z3W(5-wnX@?mD*=v$-D@KE`iNcN~RqtjRJGKoE-!=Q?3U{az>+@f+Q|*w17|SF@J?( zX>0DU$}ixCL>VCwG-RPw6Io8a%6|zJI1D#FspVN&U%|KR883Q>P8RCF$PHH@2}u!U z1ea4YIbM|kDJgyid$_OcT;|X=_=#sgo2~i2dG>!aCxp5Ep!#BCV;{zb2E;u=ApnPR zFgSScj5MQ+&vEDjpvIHg&KV7}vuYAW^FhJF^xE6lA0qWbY})1F`E}U|#ydQNOkS2z z=Y91bL_v+=;Wzr0PzK!`5BK80Rl1DJcFMrmph1iE*yR(BC#m<2Ze9Hb!GlYqH{=mx zn+^FTMq)y~cwhWQgt%cMvcRD;Bp|8&m0#!c($5&d{5GT_rTEeJIR+82t^E2Uh=Atx z>29BJ(=q>c;EWwc#=kWp*x*w}Zfo6AmUi@$31g2y=6vI1f~!r}A9QP8ebs#X-|#ut zOu{7OgvTGW>FM7or z2O>kNJCk!g@OVlx?zHtx`v^e?n$vG1BmW@{5ti$5OIU0qqN<=MBsRJmbkC~i2$A-o zq~PY>$fqFAe^r5YEnwdes8!=m9f5a*0AH{B;INyVSdzSiA?FDj!30{4GK1=5}wWrg$~)X;W96W-PTZ|+HHrXUWy=aw({ z1C)}`Ye!FmfX7qH7UqcdS&KxDtmn$pB6U|h2R4Y$>>Q9@d}C?3lysCR2jg(+5i@^Z(5#7IXvgHfP9BKX5Lj7~_9M&bLR zkX5YrSKZ9KPtml0U7w_0GszNmr$9O~O!VnhoBAW_*=RA|3#*U1+EBc4R8Z44Ck$A< zEtesHCW5vp?sT0jco2HpFJcslw!Z)=2{EIz?*UN^=raHfw*kwLwy^!$2pF&2mC|nA zB6iUbiyQv4W{T<4(PK1cPd89L-S=6J3jGEIQo4yb_*aeYL9+d;96=L^#-4`(7ZvVm zlhdeFc!XT$i({Qr4AuZQmO1JB@-15dX~}(^R;l>#p^nQ*`(@vPoZ?;1&AkwfAv)C` zU*NKETRD{5hrywO@!cIf$IG-7FfkM`ctLPe{FN`W)j$}4L*_#pO_(j_ZpqP5rpANS zWveAFU%rgXPY)peiSHc{H5)v>gO<&*7R3y)pPmRBf$SQtLjAxQpO*2%BWcTe{r`xN7szaJ~#RXqU5 zvrq-k?kl4mzhwTCdWOd^5lA_qnZ{*43ltGl>rfnN+G%CIXX?SxP?!GC%+- z&xA?#=m21$Sx)>6E}or zJr|O8P_oZZq4kGFqyU#S16Y&amnJlkd=}m1DWZT)kImzj`;7rLGqA( z_Kgh$ix+e-QTOgDG=}{Z($`|~FEi_x?xA?#uJ zra<^?Tvc=uqkjLHKc64#u%%JvoH;i~QPw*;yW7CJ#0@L=N*QuS(xCre)I%a`26}m7 zE5aN@WEUbQ z27Y8yLqn#_NAM(`S>N*>Pcpu6B#hLSmdb64%_j`Tq179V{>VjfiEJK{Zd3R-K3b?S z&nOUX&Ka#$k7-ekRp;fBOmfa=Suy2}eFZQK{3Lv`X%wBOi1I)Z8lZj(Rcye8Dz@?1 zj(&tJ*|=`qRbf5gjd-$1f$6y4S3=7oVWiHgx5AoY(7qmWB|i!xqo-Nm|IqYQ?t zk2q?1nZarQCD448g9!J2sY4*{XMJ{8^u6UoDkiPF_jIkDrRAgl@j_Yt>>e8*MIWaE zp;$#f6$CCb1OY#Uv}){Jn!E&~w%YBF09pRHD6||TO+PhgiuhCQtw3V7k$*f zMR5mRago(_AkF63iyHrH)#tH20*y-4-wMx=<-{fq2M?O9?1C=->B^G^2HFr>YT#a z9_1)#eq^m&d)=Ljysg{;!sJ38UGvUI|H1r?XPcHgd+yTJEz9WwkGAc`X6BL5X@>A z6|bCJEGP};Hg{gVlIsykNypTOYyKn_jssd~2o5FQ1_9{`Y5;<3ZD~etZf)f8YDptuMWNc})hJ?#}b{ubEg1hU>VXAd&*(xun0Gq2s^gMGa-I z31%^0rky7PGS1_75k4z_{cMGM@YE5c1NvZR*2j`kcKIojCwg}k$4%P_tDJICNQHD| ziVy{(uYe5GeBD)*RI6R@^yBvhyKdzDgqY3qA8p)lfiDB;FzJ?+M#1Bc!XT;*6~|4e zVz8(b`z<&BrROGR2W)dL0^$vA<leuk@Uf zua?WS#pqy5nCj@{Wo>;w(pvB2_1tI|1XU|bhpT>J$KV{;tL$lozVFliLD+1b%jqeI>w@8yBbE(-pT;;HGai1WaWRzRuEJOE!>T?H(2N32@WU4dtxJ?a zFcJzVwfE6Wc?vRLVq$8BFozh9Y28Q{fD{Ifl?!dZ0SQ8L?pv+I7X4s z4{>2{ctT8lrQ@sRx;WcMny$&ylJVk=pZ3Ro^*ZSc88gz^o}S3q|D9#%p+=!#TKTQd zke1W_A5(7vPIcR^4>wTDkST;HQ%K7gB}o}WRC+8S^H4}chzyx!s0EnC(*d$;E6y&vm-)vNUyj@Z6FE(6kEpk=b(`>*@@AfWjP%c zWh4;Z!NYjR5VpA4qdROXw>+Kmv?v927IW+Z#6KiX2FHRgf_Nm9l82wEtO#*DIvuCf z5WGr$VV}k=4(5L8$2haGnI^w>MR7L0Nw(4N*7+OddopKUvra76H?Rqx5iv4Qe3wyQ z|CN6$63tXhjtK)k@hv)Ta~yx(;Khfl4GLxh?;bT;y*;bRTuzlROkg{v(dc2B4xgzE zL8q~>={Nody3u>i4|%MgR~$LO#nh+>3d28epFJAd+mt2*JXX|3FNq3`HrvUi>W>K+w)kTdO5jnS(BLP&BX+? z3{wZ@z-c_QNMO4KDxgj=Sezq-Qt7?a4t5qA{y75CH3mhzk;Hr)JNFqn=4&B@Ct3#l z-7yh*Yd0(j24eYFGKID&#v!pl#;WrcP`d(*#ZZIlankfmpLyNI9I1M`EmoAJQ8rbh z;696r6MLNNP@ox2?-=r|a^16@yxnOXQNooWySa6=%8QCfLfzFOS^s_Si_VV( zE?v&?7b0m@lL{)^`nMQs0Emzb;j-yyWkPO72pD z{X*GY=n|t8&D&$yWAAyLPg1{V#O{$*STa_nU+5;psVwLO9F%Vz(dNM=&{CY^V)^vp z!}D8@d%_NYt!#yEDU#nl^F);gTo_cO?b}z5Ru_RhK+^3I`-G6>9?*dP92a%8kj}GK z@~Oaoyv#oz$J2GilTkBJh={uiaz=x01xXak>M#^bcRI$LjV=4Gy(B%!jRR2nmha&b zbUL*f##Wwsw2t%yc_Bsn9jI2IkQI!Emg+qVV|_TZ_iO6-xS)Z?&Kqu4Q2FbM*WBVM z@_sEN)>=N^Zb&pOplQ;nmLF#8O2*LWsB?%^8OMD6^$&mK^?_*jeJ2?$AfDko5G~R= z;M9%wdcy`n+e5xjHe&6i>=egP(}kY~RlKNyRBDRkr|L6e>(C1+AcnAc_=zg533gJ`QC zuL{s^0Vf%sr%#@s(oN!Uc#VV8$)d_CCZHsW`zHwMV7y3v6U> zBpjihX*Rggh55+^WCa>X;+IWh_8NG>wzJD?tp}%&={h}9#qRrX{eWa#H(j|i<%(vaFIa!{XTo}0Afs(HdbC-)iV6)Z$ z94OWztz8^AQj*a$6nexz@yZ z7H9sAQ{h-BJVg&|f9)P8$N6k%OHkoeQA%~m>KN|R`pGvBCVu0$TRzX~uyCDn&70cJ zqvs7Wjff90GFHRs)siVn!MA0;B}!2f)xG!};Mn;3oia`^8*ZZ2e%$! zg5mLORQz@2Yq@(m-`K&)`5S%mPlAE(t2ySZ^Z7+Ar z$|?-OeI)cXaUa>#)wBtEowVIAPNX84&&I01z$_N~+~un91I3L*I97`=$%fE)yqs-y zri+XX-l-r=lEg#4xpnQ*r6v^PMh899qs*U=*U#E??>R+C8VhoxGobA_2>lDB zl?e1&eWLekK=~tMdm7DjArV?BWWo~XB6(+Fe!7dpAM%-Y&o5TPt7%?~9#=f7vFe=( zNwgW*(0YVM!;Vy3@8A3LadYgVHm`c~3BUm)E#i_$f1AM`0&HsXll1ZCj(713YSE7a z>hcpX31PNm^f6b%uNj3F;S3Ab+aTSm0}?cVdEQSySC<^=HCX)ZrM%}?nU{(PhOp@( zogtj?PvIp*4Tq}y7S3-qPTWG>E1Bn=Epg6N9zk#p&fs!GcIM`%w(|;KoDb< z>Zx3CNlVQRP#7Ig$~+Wj=_2qJ00gf?`6G^7GSg7kR30%t@SqDFmttb+Nuj8hx_^W1 zp%_2;t8pz_jELR)ulsxzs2+iRQ%7NIH-CQMr&&!37n%OUKkR^J+L+KrCbGbpzN#IM+&#I8%C7;pNUx zxJU?8ZV{M@ z+rs=e-K>z~g*QklwLk!cdth7i(gw6#3)6S%O>)$4*VJSKqhwX@*w}JZkL3a{lm{U_ zE*a+wx1hx-&DcYMCW{wG{uX{4?`A%nK1e}B{ta|NcJY-ZYD+)Ll?V5Cthp4WZs;Mv z5h2PjdtAkNLmqhi2nYi4-~DVraO@1;(C&j{I!&Y~z|L~bJvwD$>O=cI$|<#{VlB=0 zP+J7Do_)W2(L(ye0DiB-Ud09Kb*{V!&g2aJ%bPz6tkf?5w#(j?OXO@TG@`12Fd%40oDF;~x7^ zk+j+0W;1^RbbaFsDQGezSF^SBY_1)GnhF%S5 z(dnzJFL&KC%uEVt_`N7z*<#qGP*?mL#=M2eg?UJRNLrk&scAux>@+WylHsjG>6?QD zi870P1yE^;g!j%z2R&PNQzl*}p3D$hp|3$^l1>P%9X!Ujf^Jh2ICVZuWO(}|&OZiH z77=jrV2i-AX>pygKwA|d*n9zPqyD3Zg|-K-dK*gPh9yGjoVwy>!l$`}AZ& z?g0&jBUuNfQVmJ$E&b9ZXH*MF$ML^ngnQ`hgGmPe;<|E2V&nlFCPvwXi67a`;#7n<;T7j&4wVSjlas{I}6hV#aS`__-YNplgDuG7%DV3V4twO(e^ z(>|3`r7OQ3o3=3h>jmSY7wvM9?7`yqO0*^K&18qwIQA@=n&fcJi2k9P{yDG@C-8R& z(y?oYVz-j*q9o*IO~>0L`eBCw%NYa%6aM-L%pFY^+RCXooC;bL6GbU7+`k2@uyI@( z7LF)k(zYbo`2%RmICl;YT5IJ5ZiMD;mY*G`q?MGIS2LToJQiq_;GX%gOKkShz}|=-j4-dtBf4!4_1P#z zH8;2RZ|aaezAfLW#zBCnhvB4xvfXr1gTSE8SdtR-26HfVL1|k8lQcuz07Ve0y*9^d zER1roB%7n4X1#^e8yHINQ@w{6$g4{(6vj>OZ{rqAXcy3#q4rG)ev@r*5=g=m zqT1z)o#{&cpq65YF(fK!0a-F)LyD(Gwwlvk8B0}wR32{62+GaaedT!=tCoi(4?@|m z`*122KmIWP!Qs$k>3OA?|Bhn4r=c23As7;Tdy>u%c#Wj$Z}O072daSg&ULZLtEW+a zPjm-N&ek~y)J;n0S=-vy!99yv>p0lnn*AD|x3V|hkHz<|9!l9IwL4pYp)=H;=Om9> zBS}a=KVz7J4P@KA`ZW4NEmI(OBvUZXYr_;X%w;-ueJ^Dx5yn!3)$;ypKJ0#*F3I8) zt7_8e_f9bpCRzb~Pft&cH40R!2Z=0QwgSZB!7}?>3j6lB%0CepRPDesz0}LlOgDiJ zP5ZOo=anjP4Ynf^hwPn&G;#jE?2Iz@jG1(T9`pmK-CmxZk+B)OR!j3(5uV*JRJv*h z<=PD#g4@v3Y$p1e)ml-F~4vZ%VYzliDAVjy)$zN^lJA3WPy!!Z*lg( zqp|V4@duF`ZbC8f7YgsXzbXQYMLesQPS42*A7j?!p8w{jDIly2ktp|i#$0~ z0IYm%I{L=iL+g&OnhKq1>1@s%YKr}X*bd{Hle69IQ~+M>*eoG*`Xh|`Lql!l*&@>T zv~|6e-(uiX^V;^9r!k%BrQKfyH;JID0>txmeT;*sX=2Yo;8Q}X#G8;J2_Vq#A2-~p zqBPEoZ&ij`KLH6f63C>Rx)-0S7$QGp&Dq|{oQKYTscmn?>zX}nAImqy-5yjE@NM#P z9*H%Lg;EbuGj%y*)#!xD`Eka_wP~PkvbR!Xx?RV>1~hk{XQEQ5YCLVXrjEoYG8ukC zY;+G#mE!%!DkkTmC=p2yovH0)27x-Q`A{UJm|_meUy#aE+C^@k=CAjWM2`iv^Cnya z6r=L%sMdsSNGubj0NhSq`Sj_dgL^hTTI1PteBpaAh{-l~rzC|1_e>;PBVQ1r$gB68 z3^yU!;*!v}sS?Xu7ny?&? z+|!(`v2(Xbp}mNRW6G9bxPJFs-j_L+KrNY^1$J14EC{7#hFnqQ<;wHFMA@9!3#jvK z(r}H{ZOB~y*g$=N?R)J5jl6Mk{0%=>fbtBmAU@0B_Kq<>58M0ol}9$jM{LGJEmO5a zwUsD@F<%0GlhSUOvcPLVn*wAeOK!f*;CAq>;!uPJS@S{lB;Qt8r_kDqaXI078 z;9p;BYBYK)O*#|xFH&^5ADQ=Lj0LW#w{aQeE>2AUeaK}GpFUk6{J6%Nv#o>ibhF>S zL`oue)P?n~-P_2`tG?`XQ<2|3FprI7IGJ1#pNDDt%wYbO^dr697fOMBlUASfx28he z2nZM&y$Et;h>KlB{ain02!AkuKN2<$g6f)?+zxgEwa%0mBbc-DtM1c+4x7HW$i%+( z+r|_3^>kCu;^{taReZ*1=amj1mayeF$p*cHj8&APVlt)Sg*u9?he8d~pznp)5K>|s z#yD!@WR;#UZQ%f8W86Mx-_ zja0um9;N{(Ap2+M@v=i-ZMd!wkH#1H{($G*%ivwQSDrG7?7nuOwzwhPUKd*F zi|h8Jxl}y*W31g~8nZdS&<%4;5CB8ZyTknOx^BSEg5Js%jc^*G&qYE?3+NuA#U_9A z$zoV!vgmX=&=o)|`#M#6D{Z>4v+I-ZA7n2DKaN?If7Xu4n@+isAZ5{>gFjr zvR4PcsTHm*&@=bf`BZ8r342VB*U_4lWt00mRY(js4&onPp;N0v3ox>g09ofmm={*8 z(WnCY0hXI}HjqqfQqSDP#hmLg%um-HLx3XsSVo?Y!^jc!9CX_gBu*qS*FgZdDd?op zj>psP!9Nd+HtBsqyNdSpob915+wTUMC11FCSiAh3$}$Vxpb2OeMdSkr7&Bhxt{Z!~ zMp!()tDUT!XDDxdhB$h{fv3j!bZ9E{Qgfr#*BgJ6Ci$URNIZaq?gyDQBc1 z(ibcc7Gi#ZY-lz!xn~q$@qlqUo_0_-2u3H%In)P4y|k<`3`$$c(q8Utxyb}03qThG zNjNtK)zF|5P8~pSUuN#ex48bqGMpJ8Z?~jAd<*i~Vf#juCO^R(5gu(0XAFR+LIs`9 z41)Jsl{>QKOe9Ml5%xR-uMf!zy?(>)L)$tkj%=kwxT;v4PTivN0a=BHDN*@dGa=-M zpx!9qHU#F8ut0$eumb~t!!n)d0;w1WVqrC>`2n54VUNt0LT)8R(cPNtH22IvT1BB- z+Z>(m^*sHptVA+J?3MhiV`ra$6BJe&YVtPNP=tIAD9Y>6oh2$YDgqq&ZxoKZR&Qve zp1^6W!B7?byAq7PQ_U zao0=6ES5%BMZJ_an9jv291^|;Y%moE{EjsP-Xxd4_WPp^*oDd#1hcL7XJt%)V)fUr zUL{&`BFH#mn8M!3#q_{C8?2x;!o{oBZW(YlU006NiADoX1JFzo1ml{hB%PS=Tpd%N zyKXWzx_o}c)>#8{NEsq9usH(rG;}ecXs`bX#H-^l+_WiRkb8xK&z*rRasx?cORy!x z9tSDDfKxmj!eyKO5~FV$iKPjTjX(^weKvTJ*rk~NXtyt2V)C*u=_KbBbj8&r8%Rcm z%(qhN21*2ECm|p$kc0?6@SUzrK7Q1Dn5@atq5rOd(3kN;o2T*wa6Zu}$?WE55=(t3 zs(ypN<^k157r~*MH}CKIIXOGBt(=Y}8 zVga~fc8VuMS*xI+00(fTgF-fbXO6ZTQX|M_lpN?-01f%ttXeOW-SDV9xYJ~_2wYQ) z2z0O-tPy){o-2GUH8HcTlh zI&a+K_l^+DXmW8ta0E?|=Y@kjFSCR#f=)ptzk%N_yB<60%kZVHn!WU^rPCV}6Wzy` zccSmxM*~~z-JdoNU-~?2WE`#($s9OLqk!QkT zI}6Is?KPwr&Q`iUr=#2G@>2?In+fTkfuj+@#{J))+}}BRVDRmJqU|#}c9wFjH$wE8 z3X-zn0h%Fhk;s&!Hyj*QPkp!C3BwK5Z~HnvsIf34_LVeEDZ z!P~_X*Mz^ZzFql`HnhdNuSAMs+35)?KgjaUZ;$FQ;HrHI2odGl_X-^1NK4iLg?^jIGB-Nk< zHY&h0a_uivO2OYTwYE+)%?7)ZvwM*JHqwUW z-1l;TW`1>mdx9Kg=*b6G@!5{{4Uc&?!#5s*3=m>Df)E7(-IOIA zQ;674c{l`x16Fj=njWu|-pxMC0yoFCG}1Kw-ku^UXIbuyF`+whgjnTCoklcJJ<-48 z#08x2=#WSpyWGKr54SR_CHfiZr?`#Ym@$`{E zr0<=81M5?~01nDVecZmb1#o9oz_%g1Ynjq8KcD9fg>E*1KtFXZAASb&udJ)NV1~I@ z3P1dyv++-TS+1@Am1#FUHD*Z~_alUjqr?E?1jCA6y%*^P(4aLBe$y2fBQ}DPF{1YX z>WS4K@$|Q-0}k8e$fLow5QkI6FmEqkk zQI?p~^6G^{0vC^X&Bd8Mc`;(PfwfMK6F!uPXCMpj^qts%YLaH;FvYCTltBMQTqid7 zDus65#8utm|7JVCyM3r*O_yItf0thlr-YzpZ~*2@E+){PtIho< zH)x9gQe0y8QU$v{lIyj&;2|O2^LYB#Pdl|Qm)gl8$}81HFj;Q9r}NrvDkk{Xu}eZ1 z(1XmQO6y)U4Rtu9#LtAek5Ce>OUZhX*K#eNsU1>fgXSih^Sgya+jClE^pB{Ij_vT88kW~H(@5zWRDO< zJs$KnOz{M^(a@lcG%0@lNb@egVsF8#eod7-3{!k`Ab;1_!l|Jm?0sY=>MYt6> z$e$s0sDYhHl7^<|09w?3Ygr92&_N*;CFzFP|D@tvJZJFzh?J~seOoDJG=sJ>Hz?n6asuu+3CFGE!zqniuWNSo z^A#PMLCBX|PXw*Hre?HPel~p%ZVSyUl0ZxboUue;H-Gn$lZDG7V7=fZt6 zSgM6oP*go7U^jW8e^~Bqa<&APOw5pbp-u*7jD31hwMTJ_RfQP0m|5#9JT3#vvYyzV z%n_wv7;x_@$)UD)|3JQ@zOPZr5Mgd#-Cm~IZ;&EMqbV(SnQV=k+sCb~ElP4{KC_9#@x0)LO3_Z59_~ zZj2J%b@cuNRb`I{_a6{zALjD_c;)xTM?^-BRj0ckUl+=EXbiI)VwSq0-eyiM~cmgT3fZ^Ys7`yQ;AX~}YCa1*0<=V#H4H3;faz@v=LpR3zu!t>3 zL>R2!WF?VLz$^YmjRWpG)zG=I$140sY`amZ2$30 zr@90S7N#y8zTIs%j~3xaoie+QDX;#M5{g(_6t!n&wr+BE9?v@xpRg2DzOP_U_@EkD z0!h?@kU2)x29q1c5bPa*o9YKl_Qgey1bQnDoh>SAqi3VXCf;NaV}3TA5l~g-+FawG z)kt0uv7_^rfh4_1_VP)PqSIY79NWqSoH)1r*o&>w)?GB-=5lJ{2H3XIk@SF``gc(4 zJSW9R3~(BPf1aYop`kw0<&E&o!Ee7L&T70MS)}LF@Sm=naL=1z>tPvSNLV9`h`$dU8ESC9wM zV|%kGd}yhLNb*g7Iup`fP9QWyxG#CGaK=kU_BRxFkUFm<0Rde5DgdMy$oUiJHAd(9 zUt}F5E^aYy!i7+8_tDuL!7g zpHStKO;24vjipdz^i0ozHl2VI8%?0}zsjve%F6BT%fu#jIpfPzJy88);L}&}@!ddc z3E4v3N3M1IC62C|xGA1YQ3x+dd3V`aLCpE& z-l4~`Y{p!iyHZ|4+6}cmj6UK>hD{mT{{{#RQ3{Rs=!g_xL{KEK+J5ZvIIm=mfdr{+ zCY{0k8IqWKYr(Bxn=+Ns0K5#_hhHQ-c(&UxrSu5rUDua3lXLd*p{OlkyWFmSw+QF- z-h)&v5LL;ucx}7-xS96^htk|Z2)W&n`DZ_FJ5YwK$T9a5Un zJJb(!e=Rj!{$}oHlYi>v3a(DXx{|w}`p&Y?rb$G-bgkJLGHuG-iu4&A_;uJCPF7p3 zSm0U%YkxfyYb%6Qs&Fd*gTa!}tS41JRC$7T<3gcZZ)H&t=^Jx)U%7erV-}_+i4_R5 z7zx}d_K}zAK{4FaklD<&H+r##;5S7|hgSUeKrL78Oea)PzO^4*6cUw8c_CPvLAMpd z7*q#{S^K3H7#N$XqXW340$3}n)WWOhEqnz;HRjBlC1U`>%g5D_nf_d5LPj$D_aaHl zbZBQJc^oYrFX~ZZ83;%)Y(V;+ zpEzXdw`ft=B12s*GiQ6Wm+9m;suMi{@UfMJE5x24PXI3cDtL+&VCCeAQ*5z@5a13> z7CF1u=Fl9*ynr+7iqEdD(5=OEzdXhj_1rKeK+KSf$u$_e;g9r^VEIvcg05+k!7LTCYz@ZFnPuI)nd89;Sn)5XSwX+gORCFv>HSy)_XY8&{Nv-Vkuv9EX$#;2!388NdiQfAppX3l<2wg(f%xbtVg&_&X#Eh$hvOAOl~bV- z$p&0Z=`QcU`2gx!ahQf|gn7#pM75tp{IlKCkdKV`_8GEU6Pe@}U5~x5mUn`;8sWoF zvixwQ#{BPt$-NQ*lSb^;I8&u>Wj1`V)8Zn*<_HPl{u%CY?m!HBF|?v8%2a2$gd z@V&y*0tg=Qpp&nB0v+^gl_R*tums%}r9jRib82!HhY9J&h)axpt@ds;UXnP736tw= zD;lDZycGNZ_o2XCNkg#s8(RXke}TjydS+8*Y&VK21N-&Q?4`xU;LYBk5D0JcU1DT# zW3$VnbHuY`U9}f+#D0N4Rwp!*A`On_gYMU>enEY!cW*H*~!6f0R|)?Oe30_G04I`&ra5ZqG-GJ z-BOgS130Oj-w%vc)Z!V>?stcWdQyDT@J5%Y^ zuSvm9-RB$HDy8 z3*Cwjm56uVxI;q$5MuK64dN}d?qXCuvmZYp?}T?N*<}kExM8;XQuR4rV@7b-;O@T( z)+%pGKIk}ydD9J{5l?czA`?$!Yfi<88YPc*w30u#Nu1%BlZIj$yGxP6ju1#(Cx@Gm z>BCwD#`X3qtjWOHR}E`Bk%1=aiWj=IIF^w#4@dzX2(F3f!=`t|s_MF*niogMw}$k$ zzPEcD_wM+ut}|RdKOFWevKH)7|0HS3iv^Nec;QGU7dVmb!x-Ltevotbh6*?l5@(CM zbX<7(?-i0tyn1($Ooz(=u~AXcnL&U_C08)ay~!}WTrQ-gP`}ntVEkUE%8JE`CpDnH z65F}BQ2bg^Rz}7Qa>WF(H5aOyV+eq?7do3`nQ#Ida^^Zg;?10eVscG7U3X zm)=clLr|F^B7o>?bEgXSB~h`7awM?2WNozkv)K)&5sO2kLB|%JO?dk=OKg$J@)4~F zW4dkpsbkyGOX!DG_B{e)S>i9 zZ&o4AiACjjm`SrXB6+5FFYw^fWBw&>4$y{cbp%Ja3ClZzBPE zrZ4?+uACHHu|qsc*hWp35u^lx8ZZuYKuB>S3o@NFvqdS8G-#pUA~DJ^cz(kB#Tm2$ zZHFp|Wyocqa|X-#p1<}c0hS@20s7GGExgA6Pn{Rp(9T#PSf_QT`m3-)yZd~$$fAu% zm<8JP+*M`kqLzhyk^uc#6$J$@qx-Ec=|#c{7+FwB92F7J9Xcx`xQ(|N8fiR((Y4Mf z-8;E3j$Jc}5d8(S`v${(B-Gk1E8Xa_ug%G?50(S|@Bq)n!K{US^o+zIAX1ndpS#`? zTKaU(B`k2;rh4pPKy+9d)6CwtyC_?-JbDo}7=*+Htq(K!K`r*1OnCuAlLd&xWrrS{ zEr_Ao<`51XR2%W9|?Q9ifW9^}-pW^rk2@3*pgQ-5-SxE1~N(_LD< z*&D0i{;|d4l+yh>L1r2hOqpuChZBk!@@p&vMbb<;6`}gJw(^GiNZ{X|T#qcNr+tj> zUTghU`-G2n>x0rKHND8vE*KI`&yC+`!11gDv;Ow4_m@`rwy{}w^nxY6NfQ13zdwUY zm*oN)DX9N04jnwukd$5A;WUD%wEIgrf7Nvi+O~R|(YEmnp5fr>*mAKSI|frE#pI0f zMqkobGO2(-73=DE*6;9;OP0KbtWjW0oI%(kr+tQ3Hk_CNV6%i5q55!_Jjt#;nE0t0 z%hHGqcAzm3B*W3ac2S~`6ze(&gWZYwS(4>SGIU2eA81}mbD#WVIQ4v9 zma^nhAu`MLwL)0m{G9u==j8X0wfi}}t@)T7psI1WnG8JZCj!lrvRuJsf^jBU;Xvnd z)(BY}ru?{v{E2h_?S|8~6tIp8f;^R?+bSV!ZM51Xb%dUXrlF~Emin-;4(HoZoTd(+ z=rXnlC?L&!S%OlGk{5bhL1>SF{t1%!^e8iFQZp?v_%y;D<1)|?{m}WvObFR&pvSI( z6^_W{eoI!fmLI=T{A}R5DcRdpwpeYPj~f!=fe4ZM5SUv00#|Lp__6Gmc!cjfd2$VuhfE7exAxUX^;h#yGKr@3t5 ziTjH|?Fg=(B+vQ{8;Z|!OPK?VGAeJhrC0m~7Nccm&do=)gf zbm!yE1e4k18)MXFsv7$DR|rSCFmk&+*@e}P;t@u~fr`m3$uyT9zjt`7a~Sbr_{pv+ zkKEml=yyz{xV3y;Zcw6Owf?(?*KoJElEH>Iy+x(^Kh)-7MyuFf%`g;tLwxUj4- z%bVGGaH`5JnB)27tt}9=x!h+IzVvAlAK}rhNM6n9khK@XLi#ypd(4_=xBvGigKJ+S zcD{315YF-R^~t8Vm1qv)ov0jLMMY0xbO1*3k4*fT^gpRgn^M8mtD{In4FL=-e{Z}z zMALq#Eg5l&wpb(K^w?Y2SyIIWAi~5_PK~`57MZ)R41TMk%P@E}3#xgi6nh*U`Ef$N z*uK!Y(})?ohp5Mo+ea=CnXdoQq2b}*bqiSeBD!%rFn4k`-KAh3DocsOO_bHPn|hW) z+V1hN`ykO5!9js*BJ{Pm{w-<85RzvsMWe|nDD3P?574k(dgpRMxGeu!K?9VFjU*2) z%RxY+6L-#uh`|t1#U63rTbOrHv4wbsuvM0^h~knE#W%&sU=$XhvDtYNNo^*Z_R#s4 zicSBF^zQ5ckctEB)U9dp$30Iyq-k-L%06B@O|WH~F7RW1HY03}jpZBYWj=p*rM-ws zDiwbJcLXzM9geX`(IiQhW-69Netzi&<{aH8XV3sDX(*ritfXWx5(%K&1OH3_UOiJ^ z8-Rm&dm*=GKvCHICrdwEI6hHF37aW#FEK6HiWvohjKnf-34H`m3H}ZHNG2Te?A-CRAv?6V zz890$)fMMIe5<86tq>ik)bh(&qGL)PPR<1RYD#Usw{*YyftAWvwb#{2zTED#h^IgA z@qxhTdsdhmN@ZJdPc%C20Z$NgN|A&XKJ+=z3Y2wZI0rMpZ=YWnE+kdzCUMt;@rYBr z*BLgfd4niH@?6K$-#-=VUhh8XZ%vm$@_l=`=-$KIv<;4kk26n&E8vkQ3+rY=jE}F> zdtQDWR07TjO$HdWC0O-KQ&y5wD`qWx+Az4E>TaFYOgLFcslfJZO?d8t_a8aEf?9@V z5fYm6r&&oi@gC1TP`&2?J&3m__{zx1*~%lZ(^$U;R|_b9mUQ3$hA(1=Vrd0R0}nF~ z(_-aXwbt(;C-E$yYtntapSKx%WuBi?ebnb5x5sXBP7(ihvD*~+Ate9_A{qf-F}%B2 znoIJ);;a)1IOGmkw9*VibR>$T)>3eTKR(SlU~c|V4U6{@m0ofpeEb{wAHDAnyoHJV zLvJP3!H(nStYf8OweeIxn;f0I$8dTsJUaJILymudCj(z zSfvjK=F(-fxc+|JT#wNz%?Mj%A3A&VV)Yf_tcH?zHiRz2o4~O=Cya+D0@8N`=df{$ zW+nWA3j7g*>n$uSPAT$<6cmS0zVwmtn1pS8QR@g1r)E5kO#aC3+`vM+j>ST)q)u5Dm77`A*Wo znw}_OOB@hGcP>63eQ|jg!qF1t>SRKvHNf(n086aG-bfDQZ5@qF_9_z1QUjlxgZ`V# z607~YL39zvKr@ncKYyIn6jI86fFM2^%?|~Vz~+qHmdXh{6=dZwrc>R2Z;&e?9NGxt z`D+3WwVTkW&4e`Ff+eft5mu9t6=<+iNjpJy@DGF9L3{Ptiha-Y{GX4HHGI-t&U0c! zN_xW)yVE+jDO^7FAr8kmar{jfl1@mvgCc7WbunwS6|ef1K&*?gjU(n9DJ9LS(T>bc68t z9=Ie@9|{nxMG-i=%7ccFysh25?k(Vx{Boqcsz@q;6BX+o)k}p(>Yv}a^6_q6j$QtNkol! zt4AxA^B=i5EPXPwyJg?7WM6PUqQ5!C^zBO=YArMNdZyT8@?)u)%eVIbQVvc}V*zQx-Dy@0KKd+8Mc= z(~}|lt30wRC2D$}OrBDyWK#@-_y5lDcmH9m{6>9=sd43E8V$pKMx-7?N)>)pU@8>O zl)Q(|8Xb)<``;|7y?6B+pV*K<`1uj+r*wU057T$Li(3}_wl0={mTGGL0)M)qBaWUt z*!Pmur9WuzU~+~CeC0J8_Fv8QVEN)&2W7H#7Mp9GFyB1Vu<#}D^rc)49-ZdZW< zo}Yng`4c}c4%o@2VaJN7|H22?$-rDu3U~w^I~~+4)s>Kenw>@G_+zZg3&_0|S7u?{ zvo1w;%XFA)h`AoiK6^J!qsDbd(k@R`h*CyFF3TAClhxIqHFD>nl3N)D7%LcZF`_Dv z7ZO)HQ-S#(y(U>0{O_D$I9h*l>3Jh@0}|GQ0?9{JgwQxCw&(#6`f#$*cD!I9t>e0n zVhUa6_SFnXhyR|4aR3^h&$O&{NB<1`(D618V0YQX0hP^UeV2;d_)ip@p3P{*wgiK1 zk|UuWCfRDCPN(kV{^afhM;F!z3$RXxR4uaJ8m0%&K6&4iZVb0sMDHcpe7n|m2i(`}~vTy{~WBiYw# zzV#MnH`G3h1Ve~g1WD#y)c9)L8R0A}TxH(uY)RBYo?d-5WLy9*{;-s~i&3W&1;U?p zAn##Ic3$2jNbo%$>?cI}AC)Jbe$rEWz2fjCKpG`QJ_bMs(}#f@VWw#j__KsH;(hfO zNPgD_jhDgUFaM8o#S&?4j!xKEjt(M7;|&hZv1$ZO)kAvt5Jaq%tu0(vgtC(()}a4z z+)Yn$?`#YB%6>F$=e|vlK#fdnYb`9YRK0F98;M4s@Ca_>bKns;5)&q*y zDkWryK`#plK8Sx3zQz&(MDyH^exxiE73qs}w|WEMv~%?}<(L%|zAoE&wlu)Que~l$ zo0Rm{nGn%sEn?dU8M2IBAZ*KhgC$$f{AX@ni5>3PB}zEzRaiMRbch^9Nl!ABmTs0b zw5ksc5l>EX5v)xXbSkGp3=0qic>*v&6KNyTdL&d}`I3|6`zTUcdeAaG@Y}Jpf%_N= z@BFT-uV4QKTpMzRn@xeVj4vmMiu=2l$MMwgW4dY^K;TLfJaQ}Jnh-1@=!6mbxAgoB zWDt;e(=o9uqPF`epI0FQM8G9hX1uYBGHHbo@YId`vdd**A)u|#xvvtoTujits6nc& z2OfrdzjmZ(H%=f>dFu@6IpphHB!5qg0E7@DDRlY$3k-ppV|Zi9pL-=iaN?rV=d9hK z3J=Df^kHKu3B!E=yyRpN#kVymxphZtM(v&ZN=B5G+Mlb(C)Ut7j-#1?Z3Xnz$t3`U zg?DmAL6(73gkF#zAa}l>Bv6HWY!pKZiic^Zt zmav0!OYmH|J!UKu^nQ8D>VOZlgA4R6uQiuCp+8t1MRh$Dc2T@?p6t$n{V!38Eak<% zX_Y4!gSFF#f#3Wuem-`$1>O=9jk@P}UxBXGs3-vCK9TmI!~lD_5PDcO_uKoC zU=4OD7{_GUoXJWO1bl;omF@Qy?o>fg9LY5%E4-urvgfq%eBX$p8K$n9qwiZ0B|t1V zK;N`_5BcgRlfBPun2I5Q*A^3&(s}LPGm-QBozN%G&wt^U32TWhD&i1U<2p&f&}Nf3 z5{S5^K!(jk0w)f``4>Bml3;P$o$rL@z?=G_qBvdcluO`@)G13IGr_npo@UN@sY+>4 z9NBQ2a(a0!-a${iC|JW10P2g{j@iA?_8Ga;&+byiV#gug?Y`xfDieQ|6(AoGSHYaq zk5EeA+J@nU(*0K3KnVt8@s2j-A%_2=BDxGeKfnLj>(hI*eQdgB=5YKSfqxDOuPj05 z(fQ49SI<~Cl!WJu(lr}+_NuW|Cp`AJ?874?5NyS}nsheUy7_?%*0{%j%v9O@qpjhV zz^d|Tu1wB5$fi&~whg-D&x=R_W~MO%{RE}(ug+V1c2V`tU28&ww#`o6RKFs^zQb84UIikHynU^y^C^Hp{d3@RH0 z=(&uH*ji(pbPH>9p7QvVf&`kqHFizZzK}W_ya6c z!YKzn>`(JHtjPB0@c|=>I3Qul@#>NT%ia5uI(K}@&|eFbR?{sj=K0PD(QG(JT1Acv zZ%RBxKNYwb9{A=J_`CvwH%BT%^9;eRM{nh6mm^IeTSQfX>b>}o^eh@;nJP_((2=rYh3mw|1-sx3~&lR#c; z(L@2I2ETWx83)pvXHKBQvozPzdVq(Rv5+W9a(&Kj;m*NY{ z=N*V@_!9_(qQ`QJvDwJzla&b<6T5n$5_XJ~Z|>tqT!<*%vk7dS%Mzt?rcci1j@+=m zn?fd>OHz!m2t~wXfOSFh>rkyA|Fsdhy=rjq8m2(ndO}yJMbc1P%om+y(8Q}nX?-%` z96@q;u?F*vm(7(}ccr=9g+3ZUp2iZVsHO92%AYz2CZLCT%{XxZX1A4*H#Ss@iwPwJSW1edgD97OBR?-MyMSVTRh0z^ z!rfp&L;il*TH*7;S4+*thbrrq}bnPg#x+O&TL z@X`SG1V9v*l;uo~IJ z|EZ2_o1Ce&lfQ$G&uFu`Hu;Cgo+WPO0A@{k)11eq15&B*TAe<98rUFK4@GM`a7(xD zyI+wQoSu}FB*XS+)sC@9`Hc3-8A1^ntXa>?T3Vl#mBkv{nC5viDk@6%whsBkyGKE$ z&iwpYK$oa7=AsZT#n;~-t4};OCAi;D;7OS=GB7ZBykb5#YNVps(UkTRsRNDMIZExW zbYjjiHMS++jUHifYvpm4xc?%;!xtOdy$`uOR^99fGXcx@d*waXJ3m=BZLvvNw2Sdk zNw&ChtZede==#>B?@ZQ&Bue?bv6q+E<=zcqVq&HfNvx$&QNR8IxP-L0uOJ_a_%?rn=;pjKKoy<>7H@ zdx7nJw1gYp*|M;(#JR^x-sj<|aCCIUJ9)X&t<>(Z)42aEM8r3W+?CJ6cV+SGrthvj z5~t>PE>WrYK!-)zQMzsqUOJXlS(l($`0t3MfWEP@@!z@rg^Tx}F^5I;4LX~F2vpFgtioXO8FiXlJygWUp__u{|O zym}xE#I$F6Pdu+Jc0qoTJCz?6_)os_jy|rZ{9|6%p1Qf8{0(P^ zJG~Frh1;GrFQ#?<1Ssk6?;kM}vF1=k4}?m@RF>v>n><=S84s(n!0Oe&)|?lowGUg% zXEZzB#a=NR`Bffy6aM#}9Bdz%O3-AoQfAzj)aWM%kE!P%%5SB1zH4y7UK##`WCLe+ z_o5d!QpxqOHCRFwGhGG`01S$vp=OH%fOa4vn$KOsaZ_aCO+D-7nJeut?O=0@Scki& z#3}k|23{d?8r@D{P$zj5ORj<2?1 zAM;|qP9XsB6+KCvv`^-F^3N`vmJP;mQ86s@P*(Yn&s{tsPe-OcRdf8_PYI9U#|!TryQ5tl zx0Q!yt&r!P3opXl2ETqiDxQ4Rx08JP|9w>GbVZ(E<&U2~P4?`OuxvkAVM)aYkNMxj za))IeR9;s81m<4TU*6m+?VG~?_j%F@*7C70`^h%sbJqFa_L)fXG&Z7cCM9WR=D>~DNMVyPDzora6aF1c zvd=zvnIK#GYzI7%RJQsRWJtt=2*yYthRy(fMpBpV!6qx2! z=CUHCK=-J6j-tses~aR~mPx&&=l&k&!3@|1Pzo_tTH-@WYUdFna&~4izE3 zb?(dQhbwwCr_Oi0eEs@hMNhD%*PY%C?_P9LzU(!kb{U6}Un=>L#p-}{U2vc%S6D?p zxx$c{cQXE8vP*D*C7XyZ6PEvAP0rt|Vmi!={gB~Rzc5>GN9BjL_Uf7M+q1uDS1D#_ zrkt}N-!`gkWXfU}9>m+XZ=(q*v$`R(F3jpgIZq0CCiEE3US@7?Zf53=cM^N;#B|Fn zNOI_s@Et$^e+TcrvU2zH1fI8l&!5~OV($>2Oe%Cz=IVe0^!%{i2~Q6XT!@;?zqnnO zdu8yLo1P{If;L{df*sLCMcR~S_F_%2A>kdcOB6r4=hx{b zmg)RE{Jaa7&i{_5+kf*#eFLmc33m*tE@>V7RzKHUf5Uep<5A!sMPe(tjO^^}K&~o! zUVKtZYt(K{LDcOUMJ9&1E5vulg&m9Q(7FwkN#E zaw^zMAss;>xe%|k@NyKjKyWW8>W%-+Ul*9dO)(Q!o7%hmEeYcMsJ)a}8-x!&x`r1s6jaBqc zWaf8!Y}%`Jr85v&R3Np^E$BH&Ht8vN9$h$8YIlv%Z3I)E&8{4~A}1$@Z^v?J*+>I|M^bSUe7Tg#6eRl^AmYwA5j$k=p|mvRYTSh0JS@&Ru9hq2^> zeOy}0185z8wY2mqW3>RZ5)ulQ4`tqjlZ}Pffet6S`IdZILw7cBt zt?0S_&NKG!ffcNZBX@@WlJX_yU7jB%^fDDX6$A|Yu*GqWd*fIJ-0M?1>66S>;n=?F z{$pl_O)M!P+R#ulkCSQ1+S%?_tgYf&a%t68v!#or@;9>!3#bQPz91#$ovxL~oaUL) z^ocp`+(kJN5fSH0XY&~8DdyFeNlkghOlpeIHM95+d2G=UI5&rVa7-R{a|<_&D=NBc zEq~$38;?y#KK}o@ z_ntLtX4aZnNBCmDdq4Y$GhFUONsEAREb8Fi^_Af2#DytZ{5EG%>@d_PCbt=%X|LGd ze>6meAI#{5c6wSg{^`S5&fGY`s0ND)+iG~Z_vr)M%Xz;6f?PQ#k(g$UzOVDmztCT{ zb4w0Y%qEr(FZL(i3VMe5d)%RM`_$+4yb&vjCa>=|n<7Bi=W$#x%lJ#Jk-yar~L+1^iX0UMp?c4(sshf$N;XneM1RJx{x=>fq&RT~TlNloLy5^Jp$n z#NEP>Sc`-SfjKufhxyXvZYad+ra4t0-)2u4_Aa6rTB(a+4J2L>3LO>Fqecj1&DYo0 z@H~WSk}xF!Sq_1$)U3c4> zX^LG8wdS*$cw&64R8qC!yCj>Dhe{l(@_28E`1k5q@yg!%_eAYdUq-q)@UP~ZuQ;v> z`nC~16^t44BYiPidnLkpfK8YoZ^|b@#6*S{fRpuK{Zb@Wh}O=jPxkoBVe1@q7W7BQ z#u|(Lx7l9tO!%#FpZ+Ka|M!6aXL9|4fr0;9R-g>%1?69#l@&GY;`dqADKp9n281D| z;(^brxi_Fs(-$xb^^72nnEen#@&V6o5$06Mo*=q-2*GV{Dq9td2bt_|Gj67hi|gOh zL+7Vf;jvG>^_vVEJ$Tk0B_R+1P zOYRTJp?(yT+;myFtAhEH2Il60$7?cI(aTi$FroZ!y+rg@idhupm&m_jgm*)#VelO1Jh$o z>#qsBRy^y_;i#9v1>}C>{&SQ+r8#=S(^0&Ku4O1ph#26auU@V2zc-uUNAzyFiRP+D z^sak|>FND=-dzPI=_WLP8p zsneXQkPmpHG~8bH{<%4ltuK~l+@a7__QN5oT|rXvSr02U^G6&{XVdbbo0Y!g|L=_8VpOqlwqhQ zDkXG}gBLbJWWwidPy9A!cy~6b!y`AALQjZ;o4OI%Tp2DZA1mlXvb^{s6mgjMH)MbZ zIV)tn zg2r&R)joR|od|D7MPOFEB(w^F5<&)!Hs2SOB zm8oX){P}a3h8-Op{rvpg-1x|1@NjV<0DEr_f(VFW`oZ!nl3O-@{QO%eX>-_m+9XpQ+=I3IDMX-x}oL7nx#`Z>eazFd;g)y1R(g4Oz{8-*-OQLiY6|GYk|SuDOIoUhg@o9nH%`2#;=pyCi1%tt zo-JniQ>ME|Y9A%4+!a5_@k>;j$$?&7q03H_V9-+cujxiZ9*~&%f4R zn~XwM!ND25;PC<}1aK&*G&NZ9ypG{JJ@zPYq~>d(A{>HtJ4rbJw7FAo3|v-PJ@Sa@80&5vVAK(tA$W|`mWgDG}<8H$M|xbAqx_NVlng# zMq|U?=#TqPsfTkGw7vO5AF(!I-XF{x+E#dQJC{^rWaktfG@1kgeEbbNuU3itNvR)m zI=VVs2uv95fINsrk2GfYYK2d(`p);QQGZEuQ&UqppjjPwics6!;*6?CZEGVdtgB0s z{K6~Krue_mg93D#MeHuXHIafOCW}tl$c_$4_LV)iW}XWXWE={}sf~|M9nma!gZ!Q* zttxB1xAkS_+2g_B3qQ18lXq&rQUL?fEjL5r_16F`{r1-7@=t0do6aXu2pT39gFI$9 z@hU1RtocL*1dv&0@cl#0|7LS`YW%@3u9baqx0D_ksavuAcU!jPqB_Wfa)Zh{dl+@( zYoTENrI~)FK!$T>tMXWEu;ls_-%(d)A$@{TYD$5|5IvqYPh0M~9eT8C+&!|GwFc<4 zjU?dMLswBNeH3Y}SQ`c2-YQ_hM}iHGnuYlybRM$#lK_}FI5-4%Tj=QM*xK64$@K!x zn?H_(?TlGT;31oL0QY;z`~>Z!Iwu7tFII(|*v7SIiQ_hR@Rh`A&b`I|@l_&vgU*~8 z*Y#rL2L- z?y~&$$6bIT~;08%DP5*S8+i%h{&IoxEOgVsR3`jkUAvTC=xrCwg-Z z{kFYNn1P$3(z?mqWJ4H3S4B9_Z?c^p|D3!$_~`+E)6)}7Q)xD-mMvWK5W@+RRaK1{ z-Esif4vHH5-PyUOrsn9|H+(cy=z5iI(3gqeTh-fD_O1h3MU%jitKMo-4aQ41L7gaJ@80O&+ZCbXBz<_X01%|R&tiRS@Uxey? zOu@EG@+TpsWuO|asNo4yOJA8N@!Yt|!98_x3aKZ#j&2Nxu5)xt^Jgqk@d?L&p+CsK zo(xT7{bUKiXDjEwPbSdk}4eUsgled*Y+#{Fc5}*=ovWY z*E^cty?fsNIX*rf+#r7bM5Y#tQpW?qhMf)*G%*whQ`?|P`0@u!d4XJHJJ<4sue zEjhk?8Q_9_M(@O1Es;K|_uufDkF2f23wz3v(o%46rzeY(8hp|y`N$1y`tr9_AF5cnEM1zo zS`LpDE_Z4?*VAiZBO!^C(c9a(tU_6N@;-8LVSz@{ z&#+cy1@erh+dPpo?1UdwpSmHWbPkIixPeXlBd{IU5uA5?IEu(F&C`$-iTKheceNICGzI5q}e6+{!=7z)T2>~}(^f?r(_*5##a=GR&y-c&6&NDw=m}J|O5vRAVxWoi^`s>nC z#Z(#>`JqvS9dc$43q@iMVes~>u1>PuDt!ieeYPh-`a4zX`R#Kf3@_-LbS%>>qMipS zOV8e>rWOazE>NWiXv9UJ*np*uAtzasJnKNlta8}ah_-E3zeerTV>4=$lK~T2p+Pm- zG=;-QovTNu7rJ_QVX~k_&je?PW8*#pcf1rUPM^=r31p?Awt3qldQSvmJsf#;UXYHV zf25uiAC%rGBjzu$2pHYHS*O#Bx3xDFE_LEQt$_$ok4noNP~UcW+LBGet*(ku#E72~ z8q-KXAG+eywB!gajz^&D>`97&DG1czIdgL(lG5tLo?b-Qc$zAq*0Mf&a35Yd2OSlx zcCZ8r?7x*hs-UPZl#N$+Zc(2YX?ZJ>e){w0Z>JYsp+Lg-@5K`?uZ$)o8)~ClPhC*Q zc1c@|&L*(yn<*fYHGH!mO-HAKL-~t%BbohwS202VE-JX=#wk9y=-%u-^g_CZK0LzUykL=gEcyloX`Le-47>`Z|L@(k0ftAP;5+R@Pg=1Wf<9dV;t;SFMIHk;Oz z`N};_AC`8fNcRD@pMo@7IVvxcMbBgx=%(c4ZblC}WNrU=4!{250s3fI1kX;1 zysC?0w2GiwDcfc?qx?q8wtbTs`_5e(hi$9UI>w8O4wJu)bJ1*BQ#E))Y!iS)k*{vskHBd^`?bQ1!B79FsLAXBxmk z)u2BU6cm)q*GMIK%xE9dvEPF4d#uYY$io8!L``LT4-bLtaR8F1KB8m>!AbORR{25c zkULUCjr-;yUDllge>)Z<@7*bS`o3GVelJ`>6u7X8sRn^&9Lu(| zvfIlHyi}^Le7cx^ZDV5&8~kms#X{oiu=+(peEX=S8AaBu?4h+rZ@@9g6zB(){xGzO z5Ao$rB!{69xw67zhw50?aTBj;uh9hR2EtD z;;BS=8^NG;e%Ws343Cw+zrU&J9o%tu>NFRKdgxkOWK- z*{bw0+tHPhb|%?mzQ}76!UIx-7x4qn?UHZ+YrUhn(p<{aak$i z|723hsx;#xX6TRF&)qM{;w9PO$XFu%=FFXb`}S?n#!msA6c`*$&yLJCPiCIDY4=q; zeq>XUj?H{h#FryUsDs)Mv$t7|FhD_zi?;x}rYoqd9Q+d0pzQi9{JJclfOxv(WdD9& zLNN~Ho!B&Y&2Ro+djX{&dRbIzL6@D zq^zc{4t^K#%zbct7Zel(S5ICH4t}(E1*L>9ZM^?DXvf+=il7Sb$247B1zR`SLZroz zI4D>t#(>=*Y#{xfV&0LIDrOIxf zG0xnQ$DW&?pGMz4z3>ow7^3;_4x%6%9a5Ra9~BjBS%;QH&<+?G!wCBtdLHOMJ*Wiv zIaWS|DLz?}WC@bDAEL&bRjRJB3dTplLz7EoKil9wT4_B8l?@nPZo?^y566I{q!ZcC z(df7y`L0Oc)9AN$jC=$C(2{P8+IPs$!oiPsO-(wxe34?+yeY$)QFPx_B$ne~ohZgp z_cRiljQ>@b{j2U1)qMLft`GYd3EZW;HL2n+%UMZ&Q-&M=5|bnz6&!k`d#22DV9_mG zbK&)QazriNzPc-2?;{)cB4X`Lx@@1qgD)j=TW-x+SyZ^BjQ6GSOU@#IOQ55Jl%HJh z8+3x=}7`o9zJE92GQ`XbbdJzTp_*wZ!~b6T?QF%{r_a*Q<(rf+(b`ABu?>IDTE z=U1=$0~xDF1Q;RYCN4p}>);T4;U`IrPb=|Ucku_Dg7^dkJEs?R0#q^mnmyRj0^B(Z z*^v>mzt$6QDU2A2GIMgo;rWJqwU?7SA>C`B)_D&$H!UqKK(O(8_6&!9jW)gn&8QY3 z3&^jwczv5Ku15Esng44#K(5|cMkS+i{(r@dT^4L7d!%rZXH z#QMbX`I)_c{0hh=$LX_T**sHSgLsMKTY)voC z?F@b7uxYP~H1 zH}O+y=k^~9XJ=>0Osa2}9AP^stNG9ZKbFJHC!?*#n~S4+tP9WJzeL;d!WEBofehED zk;tWg)AAkcu#X=Oru~MB}|1zdQPr-lL8Ad z>1q0`+2p-|_d^`|E{n3VGN9J-ir<5+C9o>n!7&{W0T#)VC?t{7O*ECPw@JFty_tGn zG>T}eRMTf7blO)p#y6?^=={~By*(YwAM2H`kyu_yt;pY2W#v=_0jmB~)mp1Uz`Nja zlHpDd`KfvmHy?|xAM)VOD=wZqJr#9X`C<8}SFp^4HULOp{@^eGwb2*+UKAA-EsVp; zb?MP&*pjBB>C}`5A%@pxc^lJA;D#+qx~5*^n>Sn(xbGSokX157rfC^3(3EBX%QPnD zWsf!4_?Dm<*KifaZHjwB-<56D2_HBK_VLd);j)mbtzv`}(S*<7P^qCA){1dsIE?Hy z$w&$7xCT4Dr4~++cwtpG`fVW0H3XYW?em1SkHR@5uYj>MT;@1M0CjeEcR|&93_=1O z3VuUZ7=izN)EGfAuui(})|{HGL{)x|l^6Xn;DYS#3Z>7(e-f{hoM=2H4v*O&92K}> z=S>m=8a?bXftY@X4!2yxMU5)9N*E0B^`F->sHhZ!4=*oalzcp`)aTh*V*CiLr4KzL z9@EJvBAv5)^6Y#cI2G@UszVh6@t)JtTNsP|4vbe@z*8ETG5q;+0DR9ny6RKb-DOy> zbz`W+@>A{8-`g~{qYvU!_Iu9|+A(v81VhTnkYEVcuMP4}5eW&rncyXi;4>p5qqAhw zamV^&hZjLooVd4y&~}Mf?~OZGF~-q{i+Xf&M}ho0pdM6rR5Ue@YFqt z4kW85Htn?~Y8vNwv<7qyL7D^{dOt~Xn8~)>;8})UqVHPJEilz$kf~+3q4WZRMz%Hn zo@u}tuzU!?W)}V?G4G7tsF&$vKKe{~L>Vg<%JhZ5h48)$>ZG2>>HCfL;#8A`a$n2* z($r*UOAJ`fOVCW7oe6Nc!wr;YfSVnP0FDa?f@S#DFSXCFhAgF2N@DO#xgJw+(A;@2%2tBPb8EIqggTe z(u6oyrqsQnRNmHKYv-5KDMe?oYftp{ypS7PT^zEVKyoZBev5EMm6H1kY%K2obc~96 zdYru+OwUSGnwpXgc~I5%bGp4K+&2LC6B1fjUTy_F8N_b?cl5da%a=)M6SO_l5alxqt36h1`eXgG#i zjj293J=loBp)lBfZhGD+r=_vNv**6>p_{<}5c;M?oRkur?T(*ye!l%9If;C=@KhTp(a zvIqG6=~G}Dq<-ug%c;9tjC3C3PeC0qCS#a9}Hb zqfU!J4oX=n2`$xJ(Y@>%31V`^kgcI-JBbkyuW=UX*RIi?Mm;X}ly?_7*^uxL3UrlJ0)f zh~J~a*u;%O&r;WGD#7#LRr5V$Bx|yQRn*`8n(KWKo+(nAT{SE@!kWkYR}VQP@;sjs zkL3lJH%cB3c*?z?vY?PHye7Uo!LYEA?-%d4SfgN}<*q(tndp+XOeDR(-O zf{rSW<19Yqi{=Swm2D@=ounm=^gQzbkWEZXAU6&v!y>s8Aa?z_+8GFb$Q~XIH8uA# z)nJ9&v>Z1eN2Bzj0a}<6ZOME9q@!l65MiHz#_?xX3jVtOBE?X)26E>1t(V4-qc!=! zc`3Uiw;+log#xgA|3ASejl5T9)7L1oA&A^Y-C-=3|z#d=w` z@)?ke;F*CD3v4@{nS)I{c-qt2tb7uaUN=Jsh0a80c8$a@a~s@Rz*Za=v2%BcQHx1Dji#f za9t@;)RSbP*g_kRUz9}PGmyqX9QOytG-OEx$pbRs9%eVd-3;f@KJ0nSWJR*Z*;dIw zEmT(Kyn!TaU<2n$qRucjlA2OoYHU!;YP|8BB<1e{aYNg7(RUH`R}FAuG=F$6XE$bLZ-XsVUHB2 z@iEQ?Td1|LFG-lNlGGo57m0Nrmc#7fOS9Xc(F$K4A#nOID7|jiy5u^7Kpl6+{u_PK z&3j9A^4;{5#py53%SF-P@{5Tx(&J$`bX-I*FfgvSov*i#*Lx$02dr*kRQ6k43{(Ow!H`FP^v$Y5R~&jv{bwNW(rsP4(6?ol7U zr~w!9odjHmF@~2xFWrXB?~u#N#RoT$>gUD;ST(^W`3c|;Zy~aYN%iG!O91$I#>K|s z{)61XJ>x4ZI{;%=8FpH65#7DzKpLos)KNn)6{xC=7Jono`5D}Akf8*y2V*7KARx~( z8Jg342N2aWvhL-!PpP7gmO)xS-(drR;eOV*JLUgS4C^fZZNz|{S5{hT;oQqI?qW@! z4B1!!_?6nHzJAomGsPG}lQC{%DXKcr6ke`NQ-`G;bWicsG%&!x@V57B6X1roBg* zcjXNwBQ`EoFTmms>$jQ<3P5nVHk)Yn?-`V&R>k1(x@*-tu=Cy~iVB_Fk)!)&e!XS> zZMBmY7n_stWKtGWK`T5W0-0a)7bY?zQ27BZ(H%@li`Lx5X$GLb+4g7}V%>$Ju5=2I=kI%HIS;*j1*ql)@YaJreQbJektodeBUM1Vo84WzoNK!Z z5sf~2m#L0i_K9TvDkPRHJ9oB&zz2v*Wi>Sfje5MsdWQaYqmOgOO*3s8gtCE*74!`n zd%8062c4|5$&)+4;F{cd^^{(UJ=Aq#n!rZo0l%}HCU3l4rJyHyhe9;kK<;yJC;SZc9DHBD=M4YS^(xvjqkS_7N_jt5eW-JoDe^AjQUS2EJpknwDQZ&D_M=FrW#>T~g zAln_ZSJ0F?Kpa31vgszVhK$t<22M%5?2Oz=v2#9RB+cNq>f=?c>L<2EU*P zOTV`8esn(&J>?u#z(0b-=yPz98syCVje-*b`QMDA2%vt8ed=zQD=l#U9$$Cqst2Yg zKytBiMwne_xvH&C=1ucbhVLqiGUbh{sg9Aj;SFwzu=UTpxS&2fvy|NFIrIe#W_C{i zL&#w!T06>uK6Lpo`~lyS?8nptUrrz%ehQ`n@y`V41mDQb~W@S1+`>i+hg!3Pj61f)ALxPl=bZTC_ zr!e8~e)~qo6iCiWq-^lo!`;)QUpxbeuRejWx(22Dsen z)%fbwafFooX1#^XP(d(`3zkVZX({i6v6PLH@lhYYwxnS)Evr!N1EzBur%35h0TE9| zB4E2@mBLZPqhT~s82e1?`y@0;L){$!5y8&lcAVnizyK1YSY7ReK=y~T#YxrYLn%%Z&uexcG=~mETiI0o>U!kZEXbqGU z71tQ$Wf2d<`qd`cyRWLH{dx89FhZd5SXn_X3}H#K^;ygMq5QYOB1SyIlud?u1nNth z53(IP$A1*R@pgLs9@Fvi$)})TuFxL39npmoEdNo&e;Z{pcqt@`h@jA9b;&B*Cy{ZZ z#3!KCqNmXZ|3BsLo6Do|q0RgO8coWA!YRU6KjfyafmfR(cfOweU%9TPhoH;%w;@W%328$Bxq|8^FkQU7yj)*o zP_a=30zY`Tq{>A6vQ>MdZppp6gx+rQM|&(*>z)c`&~p~>XNO_d0M1p@)ElHsgGL^l zC3j|ISc@n=KbF4-yS>MczeJOAfDhYeXsdrbSBMImxmA<|#XIj!x^_|&5gobKJyNlr zhTD9qz_1g$HH^=Hx%90@a@I2PU1B26^;Zuu`tZ9Gl$}(v1?XQ|NY=&)l&#)pWbUwy zq_uDD1^-slSyf5Ms>gz#6yBQXqD?nZ3dy~e4sgi;oEmyv?XF({4S^B>X;IY-vSC=# z@uZK^O9M>>7y+P?xQ2m!fY%;xBbAs!?puq6(`L8=p4Ia?TpC+TL&d^-M0*>ftQ4Oc zZoX$l`wXUWK-%piLlFx`BFO1phI80R#E3~ znM>lxTGo#TCqLQke@x+8Lr$do@jHuw@JYX(CP=9|#*SXNo9y?lS944!{}BN(5uQ9( z`e+y_O5ZNSk9Z<4kk$>bQ@_Z(oSheM>CKf_>^!y=6%Bkn3s0#-}xiLie+^Y*$CcK^rn5b~?&P?4Kf;0BYp4jZEROAUwgq^<_-Fj=t34W6;u<-+a zI5PPU`d*e47<5q<{!`ZLdIqZsv0RrR80tR5GzX4YKZfomAR{|Uvz>mA*-F-X>}eaF z$~p>tqsz0aikhkq?~QoB$ppncjnfLs(xLaw3hnAkWtyennWL{H~mL z@N72ex8jAE*ett7BSPMIGxVquY}yPHXw8A)4WGuQCSbwJ8V;bP*6ZZXOS9kJxNoe# z|B0vU?=)I2e!Sg(oAj?MqaSsQZp{e9Rb}_9C56X1!kR zn}#^e;WKo+hY+8H#5keW0gll)J*`ELrpq^iQEXvT-uFv-E9{tMPSD=TsROJV07m*B zoO6nCq%*8XF|)-c{DRPu2eI*-Q1lO`VuFl6@Iy-NF_)QDZL950O3>(8|bO@sb`Bvk!g@M$Nt5Fj4-g zwEqZ%XCP?6pL;iH2&j0M1Gk;2!Fsd;&$~QnNk%Ee4wxsklR!wQ3Hb5a{O|8Y=vj~$ zMw#S~3V={#9R_ZyYtf5PVcZZE%9oXP*?yg}c9qsK`A=CCtZ+$STr|>+9k8c47hzqGTStyE z1}hX2Y?vmH8wjMSM}Z7B4;j-_J1zD?v>xdnM&2iu{J5x98KObV!4AL3((`bWMQjFG zPxenQ5__Y_l9j60zgpQ~-1D5<>s=x;@_QHR@HC4IKf<~7@SX5yZ#m2)HRhxbQ9lX* zjrehP<_AUm{CPXPF40A>^=yN76>tR==I^>v=gpIE9@U3a+{tuvfA+1O#9dtGY;7tT z(N4x1^Lfjy+X4Sa%G)K*PxXo1K~n_R+8YU^_TPg`MuT&q$OGxrbsF4OAVSlUEd@Wqkpa7(h;PQ%!97+eM-rSZdcUv#l@%OhS;<+Wq9d{RZWr`r2)4KC z5v~V7b>HMcKkEMibCBj4Finq^>UUr%?hPXivNN3auk}S~y-RMFONzE3WN_iv(b)I{ zm?jVls%Kz8uI4d3gHh2x^io%)KRPwJf^WN2ptIQiEQ0Q3J>_4`6N)UekmIfHBBoDLPoy{EAd_~OGD@C`iYiBa2W;nmOvw>w$;uaMYU&B*szZIX z&GQMb`8q{`kOe(Fw20skOGDWS=^@Q0X&oK2=rpCT`6>3Dr)P-f+~!i`{kW; z%{u!!*IoR6&*pz_u%Cr_8fGGh#0VgvSDJbY>MYo_ z9Bbf^VWz-Z4%YI3F@Ph$sgya)bxkW?%Xq?{5SVD7e4KgVdcNm*ruCr)MfYKJIPFph za?$la*sfuyH$-K?%jxCE4b)b<)KHvcI)Gm3L-kwg%E(_5N$W%E^OhU%oBiPezjIna zvTId7kvj6R+we(`dGPiRw#DHF`AM#hfa1Nz40h%~CJLi_E}jQaHKPQ)z!h;Lnp&7l z!nNzsQ;upij`%bW2Yql&Q>j3Md9O%(I*-r|v+3I&oh46Q5I;JPj# zEc~USkR_OUZt^Gf+R_G&UCE1vM~*D#p7q%RG^`@_G1{dPGpuVWn78=sjv6!-lBm2- z*X;Wxhh~7J1=js~dVN02N`~y}74SuTzH?;^ zHZ(SZu*Iy#jknu^0R{@og5Q7`LSRfi+(!8{au==NWa;C)D@uPg1q9QG=&b;)MFKfM zs~onfLksDKrW!7RG~Lwa&tS%7V37WgRsxHLE6>6%cPh4wKcqIjEceNaefWhdh-ABF zhn$)~@?Ep|+CHmyomKV8UW@<}lkJ_XN-FB7WG`;ya~1a6s+PZF#j^n$251Cf8-x^$ zKvMvOMj%dMlA@SHC^S+wxqE&IA;1Xe;YkDkECxQb#D{i3zD?oiaUZSWwG%JJ*QlB0 zv^-`aQ<*0xISiH*XryNPb%s@js0zkwq6+>tsizN1v_+_bm0ar4_e zVjDBC7g}Gpta@0G>)|3k`iZ8;p%FOI{^zZ$NFoCgs{@JCIWp4mKk@h7`90V23QzLz zgzi?VYr+c2R?aW$*&$AjwM5~KbqN;qadQr_aW=^^ndD?Nb_rpY<=2@G+K+ zkkuk-TK)qjN}kW2<#XBlV%C$Ch%s2a7nHjGFGzSmdaeht04 zYtErX)QT7zi_zY1skS$wLx7GG7Vs801?deTg$H(NLALrKW*e=n?FfC|npRzY&DPW$OT2Te}3k1tP79NWNV2ch>yW|Ld@TH{~@}O4ZB%|Zk zoD~?Fp6X(InP$>meiHtg|BEBxp9w>WpSqTLWE*Q*XSFU{=8KSV({{R9rO-INf1J%pXIq)-qfdIYp$B#x2XAupteNx{nL+m;B-Gkr4 z9MriAk2mbTRk%f|@k1YNkm#aQxyFKY!`gv?pOGW&@UPEzjwp1D6ONn2*@x;u?TEkh zVYCOFfCnrMO8(?kPBLVE#jAF}r`sA6|KRE&=Kxc9bGrIAroXT_->1z-W4ckzo@8Wu zKo*17VgbYC>=<9z#isWWi9)yUjsCHd&vIvtVA=z=!>|ma#g9NHOF(hPqW>n2nfaE6 zC*Zha+?o$;Oa62M{&cBo{(C093YVq-YNDfalM6lmy+vW2p`*vY50qM9Re++A zCPC*s*%Ftzs;KzF<)y6FxXAqgZ7rsu>6Yq6Xq*=pU+GEa*Hl;IchqDs^fnG1{P=k2 zl`#0Y(dgjdfbbdK#iC99dgb&@aa@`Q{#G{Pq6E%)U$`mW<#2`C5=N%Do8R%zyW|_j zh$7QD_Ryr`Xv^Y>1`X;4YQZgr{m&*fX)Nh^Tt_JvXT=v(8vB?}uy~%ul@{N8{`=RX zT6GX2fYyPN?A3d_Pa0cFJoU!L#{T{ibGWRpb)TZ?Xz;NCR@!ah@?(EKqmaRnaH$i>qX6maZp-uh zj47AxbCg32;M*KQAat47p)oR^KgpfS#1oTEnvU)yu#hn1G`{5q zoBFCzkXiu!KrYnZKRxTXmUv#e00lV)c9yc zw?evg*+J@KRYyTu%E0d<#z|vcHv2$k&+v`(=UY~`x_bS_NCipYPuoBtSegJ28>v0; z^P_H^=7`BTeei0*^UtXH^>?HL2{OqZX&r*d4DNrQa<~J?%o5pqw1h!-p$Skndp^>Lwt+;s_*(!&6vJg_FmW#$#&#m0`1>%=YUh%zbCX%UNw0_5s40LwVJiR zbEZt`6M-jOZPZv25@?DRkb4I_}HscG)u7kg)C0}>_*+zl0<_7YHrt1+{w8>i0EWW^X@kqtT;vl+xX zd_&kJlD*ZwTNy%#Iw3dU7I{~b1YO9{u#s0jUU$CsB62Nr=Bg1m8xINVORmsKw4t$e zYJ%9BFHb=0I8^^a{Iqs03|*w^1dCqN&u~^CR51i2Wz~!h@yq$|VJBp~`XDFUw!I z2hX=+mDhOn$$47!1#gX zjJGUk29^(@XM?$S%dIdOF>j-zLOMz}7qYq`M4!W$Kj|w@lRgUXhlgo?UY;Z*(!hdf z6_(E;BFbbpNXVt_nFY#^6=wwgv$ERYFuxf2dm;6A$Ds4Ko~r7o_|m;2RY0HN>$CUPJ#;YpeOt#Z!!mQZPn4m{vVVCk~+Mn`0gMc}dFRdh}ytS{r@<7q&I1Byv&YsT#H zWQjahE{C;#CKZ#coEaCth+Y6l>imJ4i5xW`p04xHp>w_?OCqY;e+vTRiN@SW7NP6! z|BJ3A9{1>m*+f-E4_)5OM z;70Lve;*zVaJU7pL(CGd))mC7p|?11v{gznTrWT=nqu?mU}*rigjMH@4`#Uq%DM+} z#ZNv@K`F^JH~M$d^Tks%Zd?Pr!vk2+#J2e1*>)Cb;jRPP$qu^>Fc;;Ao zGMs{s_If0yER7-M;@VGbW*_2Al*gqf6hLmOMHYAleHX_Q3yU?dYX|K&(C*J#3RvRJ zvOlp*uDh%~2@8RM$VEZ6G0fMQ85!?k$^Y!=vp-~qLIw@B2KCb1-4oEx6nqu!p9j-4 zpr!ooe4*4>SEnUrfkZ+$DUe9hSeG?<{>9kX!4K|Oa}HeD;UtPrA#IWCgAgN+x_lo>~-Uh?ju#*x1=&{)mvq96nR1HQ{4-!FJxPuC9iA4#fGfdr6qj-X8As z3kB(vKP!}|tP~O!eqGsO)sHC2rH%^P63e0u!f4bti9;Bauv%3DWG1e!_%k!lC;UudU}Un zzn*!E1HcW&4aXzlO)pv5=B-MP$W56&>4NQLRw@=)F|fqF%P07+9U{LnX$SoTRJCRi zN7xaUg*r$KZM?ujUG0ynb`iI{EbBt*>0v@-GuG!!+CbOsCKpSd!Mo-^x4O<*o^fay zJ*CrNd+vBL@T$wrh(jJQAkoK;UkF1~$rQoBYe9gEC`wI31B@bm{P=+fl4e|7=n0;~DNN`s1t)hF%QE2)2_!vTM>yTV2h*f|7+$fcVl!s}=7F<0yb)pFh zHOqms0z)hmvx6U2Ef{yV0Nk9b%aKxkRlv5Vlv$qH8Kb6ptoMoYOq~7p{M?-846{ug z*b=~T24vYZ75FC%Mo89X5RvM4&Y24rR+Xb-djVJ7X!^lBg+iG08z=AQ9V7y-L_Aht zSR}_rPZ&%A^YLyA=(H;;v`C)7CXg)V3Ald9Vv0OQc7HbYkV;_M=&|kb(E3L6xXFvK zO0^aByawypFe#ZpGTE;0w@})PbmyYkJ6!N%w{DnyjcAG75O>v9H^41~&=u{6JBfj! zciB~ocP6H{G|REvtP&S*Vh4|iEF=x>|T(H zCp<@#c+$3!?NS!2I0i{u0D&IqgSU9l{}i}^=g>Y=($K&(OcrB_g0t@=mWw<#p-F*G z{`@N8?}-$mo){BtZBd$@M_TNJt zHKP0O`vWgojNe7EA$#6I;S=Y!v_%=h^Yr2hdLP80xR4MADQW~%KzMB~r(|5D5zPRj zk#gvFt&9M;efSfk1X&@V0wGy_(3lTWYnw9tiW}=J@ex=I(TrL&bbBv_vSAZsJlg=9 zKi!*oIXS@XN=UeS^vv5^ge>Nvnp%N8aU}J4KN~;go#1FTYmJ9Ljm{MO(tk;L*qbf_ zqvQY7b(T?4eo@<}8OZ^L_#<7?(gGq14BZF08p(E5D{j1WS`9@gTfL5*LA;0oCTkU6e|ISTs$rhy}&RGaxg$A2$|Ahe=jClkw^4=WHBNL7M1qMEvZKMn01AXI@UvR z4?38N%1YSgv02BA^7&|DVVnZ`4@`3y_{NJI3H72ym7P7JWb z7Fwyb&mLyMFRG&3w}sGUoxoOy8I~n0_-=bFyPO*n;=`oJ1rCL_3XY(e)Jb@Vx5ogU9gM-#6;>2jo^{4l zu73K2<)JjEZe8_6a?lBv@I|1`mUD^~Su(SO6xNN1ria-X)BLM5M68L3`?qg@b8m)~ zRCut53|7xHHGqKvw+^0r>kCLB@aYRdPRQI#4ZA(BZ~3A?%(Z0j{ZlIb+&fc0F_n@v zD~PCqJcm4#g~uaB;@mV*ZjSc$WuU?gm*;**;u7a8I*0ZjU!+8f(TP8Wp|az3hi6Li z*X|SBE}_@PojtF|P1=)&xMb<^PCB-~y3%v0pHiDKcRinEf90rB%Fbu!$E>J8k?;97 zYoBE94%^U!@3!-~HxUta8QYyR$?Tn1j@gKNO;}o)$;saRR~B%YTsHypMYU_cc-qX0 z5Q_?ht;oFvEC)s$fKeyrT=#~)5rv|%Y^iBQ8>^Zg)0!_7D&KqH7%=_uqyFvNq2(N0 z27lOolpqxl>H-Se>Aqep^jKtG0~sW4v`yZqCbrKAp{ISG|B-gS*xT$aVz^6$rfbs| z?A`r+O-u}ADj9=m!?tiqoZW%sG;5YW8_{inG&Hh+@sfQ-ULJUaRBEX?*hQXEa^muu z5R0^pb0{>^sx*Oxlm4k*R4q5B>$kUbblgrqe*eV{#uBqmcuz$_0FLK{T|(amki0Zr zHVHY1WPS0?>l<&i3!9qM(lqI9Uo}Og3CYnuRP4LY(;5-9YcV5l00WY!eyFvGj|JNr{z7quq7RbW zh1pd-&g~0TEV@jAjQKCrBGAT%wJOYN@srx45esi{9oMviwmDHNga`-30Tr)aJptYH zo}=R7!LiJbeORI-TxS1-0uYuQrY(x)VQ-PZfiAE3{`;@R-^eK(=>eBUWws&;>~3qJLJVITy(~;d{3RkZTmP?W0V~hFT;rC*ur@K~w1lnB z>8J{IvC6f9D@mrhM4l-Aw!~Tso>g*DE4M-D(LzOLG4EVsR4`uldTUyn`xO=zJFY5P zP9LWEuzy5709zX1kTT;ZT@aq=4{VlP@}D&S6S`1p#JDP}oB^^X=UH*cf1XpqUzm48DaGLcJ`?s@FC6 z<)4uS(TmpFXrSNu;h(uT8^c%i!@bt`=UQRbsQB-F+800v!+QRA-TJo7T2#Q*x95rk zco82>n5+_}XuaTF(yjjk9u)yCsG}ng22KB{W3V6{p4>8GD>_1AU6uYKe^pHV(i^UD zB<0JTrTwZ`ueRZ+wXuN+mrp$-ri|~_TMp&EF@;FF)raOv$;Yn;Ned{g+92Z|loq=N zBA?(X11I17;VSAvjKIR~lo;s~5%6D3O-0&TCB3e%H!%1_6wmN)u=1}# zKpJ7M>K}wB7l(g&J$L*>cdb5=5kB-`+e052AbR+pAFrE1bR7iRLcntvcgiJz`QS{+ z-{H-?Z1e`0=MB!rc6L`B4{wd&XY`T8adc^L;d=19?BEFeR>;JXYY8*jXBFFPmMaR+ zE<;kO)Q6)8)X0h)uC2|;+@jCH@+(h0%_?`HI_-opbuzA#qoe12@3(U~IXNt&iI!lm z?db_aT3TC&4Tyjl7&1}$X;isO4s@Ye7jeyhrr<}f9`DsuqXW@z6EwnlOh1X{0CEPd zdbVm~-Bj?^M`eX4Ek-kv5Inl?3P>Kny^)ft$h@wqf~X-n5@18Ft=ZyHz~aRbY4xCY zD0tK@yMkeE#*OBYqT;P!l7ct!p}BwwgrBLQfnld|w>T>$bk$sgn_}J*CQ$bN1Zj*8_eY>Hf!FxnJ!m*3=4!{vuAo&^ zX*_9aV_|HB2=?aroQ!;#G;E8BA%M9V$muEX5TnLn7t={RO;Tps+}e5!b6t46_Owcf ziy|4L2M`-&N5iR5IhCYpBcg~`4}A>`y8PE9$;z?)$}LF|9O3UK z7>M!WBrox#_?1(VH+^1QT*Pp!F$Nw`X}P;6m+^$?rzLlZ;1TK;7=W5-$qlH*g7;hA z0CfjNWYGtNZ|)64yMR_ngsrQ+XuNZEffNsmJ#wJ5t7fe*l&_#5^^iZTlT(L9nDzv( zZd%u9{JzEm2jb&ly}T+U&gu_+PAkhUHbrgJ4xg}CSZKdY)6mr9iE;li zExb&5Di<^lq(!9K(_<*H?ihT6BUzg%^l=b)KyF;vRUYH|?6OCFiV~el;6A}hd7K>R zODZ5p`OL6J*UD>_1Us+vmio==#HLIDsktF zQBjrq?{`H;H`cCCTKHxhm_14{zGh}1+(F~h%yJ}UYMGjtunw+AmPy$p6_&{Wa79rJ zLVts7`w!K;YpREqx*^)emzeas@IrjVc=%YiRuQe5D|iD&*PWo*0Z5~&D#CB8gQ}Lc zFGN=_J%5}wqP`#>TBDRTYzqm|peUSb|5c9>mtm4v_wv0ZWji482C_4c;fMu=<{lBX zybKiHOKwI&mI*CKR~Rn;+o?;5FsCZsptE_La9+53&$*cQf^Stt-QKsu-N&IA@@K<&aa<0JC|`hJ^Yse*gh%haik zQ8O~Hc)+}g`$|APYr}A$4K-A(kve4S+r@!_&epZ=ZRYsZz{43G0p2KfJX8SojCmHnp39g7&rR1k%2Y2O8 zCH=7y+ZcXl0k&Ty2YNT4@Xpc*d2cI{1s)2{xc+SYc(7N`_>4nd7G_VFxN!Kw=u1%5sj>7WtOVkONvoZ$_0p_yG zXCBfW(p-9yx(W~YLoXH46&0FD3gSYGqdV7yyMVIRk?(P$# zn6ddtX?Zns0z|dY;+&gngD&p04G)G%=@USo5kYd>qs1unjbtEcOel_Lf7XAft-{%) z<_;H~&eMkyt%3*N(!h zn(cF8p?@`H^C@+?AC_LjS#@&KvJXn_Ce71r zjTVvo5%W36jF_ zFQZ#3lYiEe+SX!=b67i4Mzq)E=lR=X&GDdS{OJcx^hN4OTCPAr?r~g!q8`!~aew{2 z#-~d>e19h(oXiPN9#OQ+7qk4+Z$Dsh*V+?*mN0?&W&q%&P65#j3;NAh+(fl>8x}o_ zHi!QqiMxA?kY=6bWuOSn)|b)P@>?QEjPNo>^2;F>Vi@ z-(5&+R?PMQ! zr_BkiW|RgAq3)PDOImvyHI|}n@~CJ-Xwz~Piy=}fDNgfy3KYfm_G8BA79@_D97EUG zm=1`}4+t7aF2y4%GXIlzHagF%Rqe?p^$Z_aqU%vmIJ;!I4M)gvgUGx*iY6CL3&mLR9FG+$7WAP`Z{W1( zrbS?ZqrYyDzUKEVHr!H2PwoEf=v|hupyZ*BC~_l2;kGA(bALwS;A&!|_D)w6bC-5O z&op6f4W+VnW$^11ryv)Ro1!{?76)o9?k-$01av6ZFIhYM6wXCc%O4QJ@M%6P372QB zn!;?_U9mRGO+D;P!7;2$vX-fOPCSTZeV$G?Do_=G4SW7inU!5Ue`i%w2LNzsVxO#u z=K6vl5)%{wVIj%JaDTokFV9cMNa0lYN&dG(*&2CB*rqS-zH^oBg>kA8Ur98CX%zE! z5+dJ8%gP1-G6JSR>)VYB=YVKge2RDVcJaz+7>U9Q{1|qt&mfP0^Eyl?;Y7x^<%S$U zD*4xQ+Yn5IrIi?)u(O2^Aj6`!q>U@71y#p6Q`79$tAq}pNMb*~*O8MTBGWj3{r7nt zW3H2*ewT_}f8rYoUYa6ZA32Ytbdu>NN_;CjRy+f2&N<3%7TC--SnlAUTcKyIhi18v zNLV=0{NeDkTa7Je76J9z#-gqad(f?~q*E$+WcliaKT)!C>)yf1#N59!+bc);S)7?% zvzzyc`uuZe(p?i@$93y9J(~Gw<`+3695a+z*uKMH*!AgS?fUY|Pp7Tl?vey@W~H`& zV0H|+e?;x_YRI*boAXl&mimI8K^j8J6^p2PtGJBFzyPGHgR2$gz!r_e?MtQ2M@$c|W%R;b1%7Oujj20}Kbl9^-J5OwjWc6x=(h+B3qEKkRm(9P+649r<+BJ69$2r~l7CQ3ysWIejp772ZlSJz;kek-p&D+%k!`O-5mtY$ilL()OBv<}zmJ-ajMs6$ za}s^Xevb9rhN^w8)u!RPW&3sJ`1#S2OQT+=GbMurqDDk##T^B)Ds}Qj(#E&OEl6$D z@vUx0^d*-2%uMUA^c=Y0btwMF!4`wMLHr^cMi9Xy&tF4y(mO45X%fqTlSooc1NSQ; zU8m#eZ?&SkO0Rq91;>O$2InQ2?wb6PJuo-yE+@T~ilAIiUmI6BDDh?Po*6uMIDd#d zBrJU!5$V@JFhsGR|D5f+Z%6PK3u1%5(4H{#EC0HM)sP*&^JglOumFX~rEMk^Jly*nSsYUn$KZB1||{ZU4m#UO8IU36#N9HWhG z*)RXTv?W9xzANsLp~AO*b#04Z`UF{D@-82q@l4T&5)SgMC_vw=fAzBMqMLZP8ISPu z_7TJXs>7vIoLXmNyAq8xd6qwp60qB!$I)r^tU48&AdZxSmq_Cbv5lLk9hNpY9f%~*E1*cUPKRfIPr#j9bvG#PF4tDVSWT@%W zVaz$00ReQg zZoLq;!CrzW8v^2*4DBX|bO4Pvwc*1*1vuRWDo55h?bWGIm4&E=l9m;&spgMD;iQhj>cM95NK=ZMnsL( z4pL?35mAA+Jv)@(vU!Q=sbPSg))g&Y?x1^uNusxFj6g`uG8)wrfe+BBIHkhHp+7{m zVnDtLZAO{N6vQsyCvxHw_TYH_NLQiFMEI?v+7?bLqS|yIS-kG3)-k_%bdYy+EvG!k zBU0(IOF=M!$8l;I{{|-dk^)1miW~oqZOmk)aafS0W?c8ZU`MO=Ar;U==_=xH_7_&4m*e5E$wY+|#RmRWQF6|A;nfd35 z<8N2H(jFAAWB`2@+DYU)K?*-}Py2Icy}mxcAU|MJaGTsG$G@}G#K@h^rIw&nT%`?r ze8F9}<)D6yr36}a|7&3KXg0xEY<$;qAngrC#O`~GUEm^zCxS%}K8`arr zO6S0w5+j9Y32TCM1NY`xkRV>P?LOJ9G&}na>ARrLc<^8wxIr#8MwkhzeoHPVZzz~! z?&+pN4D)XYeQJ9hbV--Ddh9o%gmVsz55>UvIDwunA1FJ4jM2bm=IlOg*Yte8A!}t25Br=G>Ud8TOIMhU?G0@H^O&3S2a5 z-^-R*LYl8Vw^^#qpHl0$P56?Rp)uw2+;{ikO^NQtKW8_V_R1tiA{cz;v%KD(^@7C+ zW=`C1f=}Gw0oj_W3dBJEf0k>*(Ri;I>7xvz{^94>Zh{stt^J=Ly`nps+MFX=ku3?$ zCJYhZ5)u;7>71DXW&{&aIKah#Hxox1j+}A+yy^}O*DI9K7YAnNu}eD5COZIUgEUD{ z-ar8tjecia2bUDG2_SRyamVk+kicApX?1%_KUw)efqC8@o0%=ZUxaSr@QyjpHa&y} zOqaP3O)Q|U7eBy-hN45ebW%Rb(A2kg{qWQ0&up{m=~I=qu+Jq0l5r#e>&jYnUZ120Jz0S@~Q0&$K{@?;M0wB3x zfO7f!r^;=wzu>HC2Uk86-{4uDcH?%7gNX;c8b~$pmTK(Xh*eSz5)h8vuu0*1)hRd=u`!>vTj z3VauskHJ>HN} zp-de2ijpKR?=Phb4230rMmPQ7!DO{biyjpinOJKQr=+Xf`1EMRE2EP(2)AABFI_m_ zaUpfmuDc&Fb8Dm4OjxGX*7uH(qIW|IZwaNWzT{Ky@=W7C)oMekYsZszp9>2FTFX5( zh*(NH9TYAmr`C7M$c5cUloBn(JJP|vGv6IZ%HP9oeIJoDy54a-?0Syj8M-ZEedej= zhJ2Q!7q>T^{^1UlvNN%6_ms@JfCz49V*}h08{fa{8TLxMioeq_sS{x?1_)C)ggkX}0*<*%5 zA8?H(wt3Elfr``z9Pf}9SK*F2GNP5PafR~OV`F5+~KcjvS=DN$r3|}tySOQ)7GGD7Q-xU z$Hcwwif~U5sr#-DOYlwD(#hA7veo5Z8=y4T70mo^5#Rh+o{|PJ6qaiyev(C0J)Z01 zI=bf8$4Ab?d0~xyoYO8`hv>wm$y9$M3OO1Ji3w6 zKfUcW`y1~3IzD<8%=vSCvFGXn#*UbN&joHNP`trMJ8tg@fgC^2bUwC&gZB9_koj6q z;6N+GbWs|3a^*0eB}B1s920mBPCB=@b1I>Leuk)@XC;rkyj;FEXzHeduJ_3*pV|K3 zGsvTX;87j!Hw_KY?f+$mqj2lEPWH9)0O`3WevVwwup?rceUQf$cr9{2YB=RiXQHEVna? zPYlS~QjOxphvoB;PnEitg!)8H&s$#q3$@kKt46YAX)M=Af5DbVXikoRexd5gwo_U= z*T11!PwO3KxDXE>2&i57$}ly4g_Q`yK)Ze&mbksW>dDO>GPrsm1*+4m0G%zA&){-Q z)hhi8UthkIl$K)l9k9X1$HhU00;Ix0f2Z+l$Np2-vWgaHFMyfKzySVbX9{pJE{qZT z^;J&*PH-RW8-Jz_Fv9H13x5dC!y6kLp^bP7qjlgLAcM~W0*kL37vMBO$~E%hbR|Wb zh&7~Z5r~>~Ag(hPk`xz*NwU6yqHJ}(9-k~rLZuPEgqYaNdyOzOq+V4hqjSyIxy48k zLntxCF8qXJ)VemIBs!q@vArpa)}YM|?#`a=-c|t*juEeomt;D|#*gCp&APq^$Tf-) z7wK1J1{!9%-|p4Wvm<-Ap5NMfBLYX?gQ3ffIzqrE*9_Nql^EOqEGoC{NAF0k7iDO# zBZ~3;(9>bFqU}~~+poqV7Fa)DI~?)GfM^cP2H`IiVj#l@FnRGHLQ3ACf(TLw%PPd8 zWK^Df1G8g;lMu`%RDmk(oLz>gC{cIu6nLJ?3asKB{^svueMfR+?#FCXHdJ;i)#h_?nJ5BPb(E8^W{gSx3T)1nK zDIV4=fl*A7_jcy8E;U^~$j$)E$1ZT2I4e9@jT!5A+K-B_-hiSW>J*4oJ^2PGMp(UZ zR0^z9AHjhC9HOGDj`08CO?kBB&LJ1Ex-DmPB;m%dX-vJFXtTQ63DN0iPhI`Hk-0pq z`_Q>sfLazRo|Wn7IDV2v3=(p_4ZuxzUwwvSWCw%w-mjyhmu4MINv;NxPgbNU$;Uaa z@jsLhBE{U+3Go_Sp%-KpkFt;cK6|GD8%E_m9q z(Del2fzhUV5@tSW`r&|p@>2-7$za)mP<9CFdC%z0La{%t%uFpm3)s5^2y+5}Q44tL zA!2CQmX*wkw79+f95h7y>8HL@R2FOjYo6$KA+g_eUVXfsA>~SFZ%9>!g;0$2OO<5> zJFjlIq;cDRVZtxb2XYm9uRqk~{4V6{Bc433!$(PzNnOhw4ZQffz|742zA|Bo07P1l zI#HpLYWb(8?90cGp74e+WZ;UUPniC!{?!#{gMzzzJyhB-GJ_8eA&-0SEntjl+TOm! z;AapU_*J2Hsv=nojEpx#%QrXsoDEDtbAev0O}~jby2XE)Ldp4=1Oso2T73^-moC-LbS2A!Y^N&8wp$)IYDw{%(dI z|98woy|`!&!XHC%kLFoWV!?IZg&M-skEW64PBOC|B=^8=Zitymm9*z;V z>~=Jg+n_4OTJP*xQv9ex_nmG15WZulr7%-~8x-s$!8Cy87jcG-o6p;lbv z|4KJKNaIF=!8@gUX&R!l!50n-1+MqQ$Zpm!kZ8$A!P-7;Y0y%fCTGk@cW3%g<2(Z{ zwdB{}k7g@D+%XfAlc7asy!X;Z*Z0*Rba}>^W*-g7;H&OXFVA`}Jb|#&v~~pCT9+%`5*P*J<)=K;g zKN^UKuzdgrXJpHf{*T*CkGZJbnrw?*du2N7-;9h-x&N#oa(IB*?z~DvW!IQ<=ZI(%g(MB{qHPm7K3z9xkI4`gYY;CkYLG~;30XacKiCvg=jAU1_HsI>G7 z65;> darknet.yaml << EOF functions: - scar-darknet: - image: grycap/darknet - memory: 2048 - init_script: examples/darknet/yolo-sample-object-detection.sh - s3: - input_bucket: scar-test + aws: + - lambda: + name: scar-darknet-s3 + memory: 2048 + init_script: yolo.sh + container: + image: grycap/darknet + input: + - storage_provider: s3 + path: scar-darknet/input + output: + - storage_provider: s3 + path: scar-darknet/output EOF scar init -f darknet.yaml -or using the CLI parameters:: +Creates a Lambda function to execute the shell-script `yolo.sh `_ inside a Docker container created out of the ``grycap/darknet`` Docker image stored in Docker Hub. - scar init -n scar-darknet -s examples/darknet/yolo-sample-object-detection.sh -es scar-test -i grycap/darknet +The following workflow summarises the programming model: -Creates a Lambda function to execute the shell-script ``yolo-sample-object-detection.sh`` inside a Docker container created out of the ``grycap/darknet`` Docker image stored in Docker Hub. - -The following workflow summarises the programming model, which heavily uses the `convention over configuration `_ pattern: - -#) The Amazon S3 bucket ``scar-test`` will be created if it does not exist, and if you don't specify any input folder, a folder with the name of the function ``scar-darknet`` will be created with an ``input`` folder inside it. +#) The Amazon S3 bucket ``scar-darknet`` is created with an ``input`` folder inside it if it doesn't exist. #) The Lambda function is triggered upon uploading a file into the ``input`` folder created. -#) The Lambda function retrieves the file from the Amazon S3 bucket and makes it available for the shell-script running inside the container in the ``/tmp/$REQUEST_ID/input`` folder. The ``$INPUT_FILE_PATH`` environment variable will point to the location of the input file. -#) The shell-script processes the input file and produces the output (either one or multiple files) in the folder ``/tmp/$REQUEST_ID/output``. -#) The output files are automatically uploaded by the Lambda function into the ``output/$REQUEST_ID/`` folder created inside of the ``scar-test/scar-darknet`` path. +#) The Lambda function retrieves the file from the Amazon S3 bucket and makes it available for the shell-script running inside the container in the path ``$TMP_INPUT_DIR``. The ``$INPUT_FILE_PATH`` environment variable will point to the location of the input file. +#) The shell-script processes the input file and produces the output (either one or multiple files) in the folder specified by the ``$TMP_OUTPUT_DIR`` global variable. +#) The output files are automatically uploaded by the Lambda function into the ``output`` folder created inside of the ``scar-darknet`` bucket. Many instances of the Lambda function may run concurrently and independently, depending on the files to be processed in the S3 bucket. Initial executions of the Lambda may require retrieving the Docker image from Docker Hub but this will be cached for subsequent invocations, thus speeding up the execution process. @@ -44,72 +47,66 @@ After creating a function with the configuration file defined in the previous se scar run -f darknet.yaml -This command lists the files in the ``scar-darknet/input`` folder of the ``scar-test`` bucket and sends the required events (one per file) to the lambda function. +This command lists the files in the ``input`` folder of the ``scar-darknet`` bucket and sends the required events (one per file) to the lambda function. -.. note:: The input path must be previously created and must contain some files in order to launch the functions. The bucket could be previously defined and you don't need to create it with SCAR. If you don't define an specific input folder, make sure the bucket that you use has the following structure: 'bucket/function-name/input'. +.. note:: The input path must be previously created and must contain some files in order to launch the functions. The bucket could be previously defined and you don't need to create it with SCAR. The following workflow summarises the programming model, the differences with the main programming model are in bold: -#) **The folder 'scar-darknet/input' inside the amazon S3 bucket 'scar-test' will be searched for files.** -#) **The Lambda function is triggered once for each file found in the 'input' folder. The first execution is of type 'request-response' and the rest are 'asynchronous' (this is done to ensure the caching and accelerate the execution).** -#) The Lambda function retrieves the file from the Amazon S3 bucket and makes it available for the shell-script running inside the container in the ``/tmp/$REQUEST_ID/input`` folder. The ``$INPUT_FILE_PATH`` environment variable will point to the location of the input file. -#) The shell-script processes the input file and produces the output (either one or multiple files) in the folder ``/tmp/$REQUEST_ID/output``. -#) The output files are automatically uploaded by the Lambda function into the ``output`` folder of ``bucket-name``. +#) **The folder 'input' inside the amazon S3 bucket 'scar-darknet' will be searched for files.** +#) **The Lambda function is triggered once for each file found in the folder. The first execution is of type 'request-response' and the rest are 'asynchronous' (this is done to ensure the caching and accelerate the subsequent executions).** +#) The Lambda function retrieves the file from the Amazon S3 bucket and makes it available for the shell-script running inside the container. The ``$INPUT_FILE_PATH`` environment variable will point to the location of the input file. +#) The shell-script processes the input file and produces the output (either one or multiple files) in the path specified by the ``$TMP_OUTPUT_DIR`` global variable. +#) The output files are automatically uploaded by the Lambda function into the ``output`` folder of ``scar-darknet`` bucket. .. image:: images/wait.png :align: center -Specific input/output folders ------------------------------ - -If you don't like the default folder structure created by SCAR you can specify the input/ouput paths in the configuration file:: - - cat >> darknet.yaml << EOF - functions: - scar-darknet: - image: grycap/darknet - memory: 2048 - init_script: examples/darknet/yolo-sample-object-detection.sh - s3: - input_bucket: scar-test-input - input_folder: my-input-folder - output_bucket: scar-test-output - output_folder: my-output-folder - EOF +Function Definition Language (FDL) +---------------------------------- - scar init -f darknet.yaml +In the last update of SCAR, the language used to define functions was improved and now several functions with their complete configurations can be defined in one configuration file. Additionally, differente storage providers with different configurations can be used. -This configuration file is telling the lambda function to retrieve the input from the bucket ``scar-test-input`` and the folder ``my-input-folder`` and store the outputs in the bucket ``scar-test-output`` and the folder ``my-output-folder/$REQUEST_ID/``. None of this buckets or folders must be previously created for this to work. SCAR manages the creation of the required buckets/folders. +A complete working example of this functionality can be found `here `_. -This feature also allows us a workflow by setting the output folder of one function as the input folder of the next function that we want to execute. For example we could have a function that parses a video and stores a keyframe each 10 seconds and then have another function that takes that input and anlyzes it. The configuration files could be something like this:: +In this example two functions are created, one with Batch delegation to process videos and the other in Lambda to process the generated images. The functions are connected by their linked buckets as it can be seen in the configuration file:: - cat >> video-parser.yaml << EOF + cat >> scar-video-process.yaml << EOF functions: - scar-video: - image: grycap/ffmpeg - memory: 1024 - init_script: parse-video.sh - s3: - input_bucket: scar-input - output_folder: video-output + aws: + - lambda: + name: scar-batch-ffmpeg-split + init_script: split-video.sh + execution_mode: batch + container: + image: grycap/ffmpeg + input: + - storage_provider: s3 + path: scar-video/input + output: + - storage_provider: s3 + path: scar-video/split-images + - lambda: + name: scar-lambda-darknet + init_script: yolo-sample-object-detection.sh + memory: 3008 + container: + image: grycap/darknet + input: + - storage_provider: s3 + path: scar-video/split-images + output: + - storage_provider: s3 + path: scar-video/output EOF - scar init -f video-parser.yaml + scar init -f scar-video-process.yaml - cat >> image-parser.yaml << EOF - functions: - scar-darknet: - image: grycap/darknet - memory: 2048 - init_script: parse-images.sh - s3: - input_bucket: scar-input - input_folder: video-output - output_folder: image-output - EOF +Using the common folder ``split-images`` these functions can be connected to create a workflow. +None of this buckets or folders must be previously created for this to work. SCAR manages the creation of the required buckets/folders. - scar init -f image-parser.yaml +To launch this workflow you only need to upload a video to the folder ``input`` of the ``scar-video`` bucket, with the command:: -See how the functions are using the same bucket (although it's not neccesary) and the output folder of the first is the input folder of the second. + scar put -b scar-video/input -p seq1.avi -To launch the workflow you only need to upload a video to the folder ``scar-video/input`` of the ``scar-input`` bucket. \ No newline at end of file +This will launch first, the splitting function that will create 68 images (one per each second of the video), and second, the 68 Lambda functions that process the created images and analyze them. \ No newline at end of file From 2c6f2cd8d715c9e311137bfe369fe9d6f9c36b6a Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Mon, 23 Dec 2019 13:12:50 +0100 Subject: [PATCH 39/41] Save 'IMAGE_ID' in env var for ls method --- scar/providers/aws/lambdafunction.py | 10 ++++++---- scar/providers/aws/response.py | 5 +---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index c96c27a4..ea2c6575 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -83,12 +83,18 @@ def create_function(self): self._manage_supervisor_layer(supervisor_zip_path) # Create function zip_payload_path = FileUtils.join_paths(tmp_folder.name, 'function.zip') + self._set_image_id() creation_args = self._get_creations_args(zip_payload_path, supervisor_zip_path) response = self.client.create_function(**creation_args) if response and "FunctionArn" in response: self.function['arn'] = response.get('FunctionArn', "") return response + def _set_image_id(self): + image = self.function.get('container').get('image') + if image: + self.function['environment']['Variables']['IMAGE_ID'] = image + def _manage_supervisor_layer(self, supervisor_zip_path: str) -> None: layers_client = LambdaLayers(self.resources_info, self.client, supervisor_zip_path) self.function.get('layers', []).append(layers_client.get_supervisor_layer_arn()) @@ -192,12 +198,8 @@ def _get_function_environment_variables(self): def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict: result = ConfigFileParser().get_properties().get('aws') - fdl_config = self.get_fdl_config(aws_conf.get('FunctionArn')) result['lambda']['name'] = aws_conf['FunctionName'] result['lambda']['arn'] = aws_conf['FunctionArn'] - result['lambda']['container'] = fdl_config.get('container') - result['lambda']['input'] = fdl_config.get('input') - result['lambda']['output'] = fdl_config.get('output') result['lambda']['timeout'] = aws_conf['Timeout'] result['lambda']['memory'] = aws_conf['MemorySize'] result['lambda']['environment']['Variables'] = aws_conf['Environment']['Variables'].copy() diff --git a/scar/providers/aws/response.py b/scar/providers/aws/response.py index 3af117ef..5cbb5ebb 100644 --- a/scar/providers/aws/response.py +++ b/scar/providers/aws/response.py @@ -134,13 +134,10 @@ def _parse_lambda_function_info(resources_info: Dict) -> Dict: stage_name = resources_info.get('api_gateway').get('stage_name') region = resources_info.get('api_gateway').get('region') api_gateway = f"https://{api_gateway}.execute-api.{region}.amazonaws.com/{stage_name}/launch" - image_id = '-' - if resources_info.get('lambda').get('container'): - image_id = resources_info.get('lambda').get('container').get('image', '-') return {'Name': resources_info.get('lambda').get('name', "-"), 'Memory': resources_info.get('lambda').get('memory', "-"), 'Timeout': resources_info.get('lambda').get('timeout', "-"), - 'Image_id': image_id, + 'Image_id': resources_info.get('lambda').get('environment').get('Variables').get('IMAGE_ID', "-"), 'Api_gateway': api_gateway, 'Sup_version': resources_info.get('lambda').get('supervisor').get('version', '-')} From e71519d4094622c79f1490453fd64255ee1ddf0f Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Tue, 7 Jan 2020 17:39:39 +0100 Subject: [PATCH 40/41] Fix creation of deployment bucket --- scar/providers/aws/lambdafunction.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scar/providers/aws/lambdafunction.py b/scar/providers/aws/lambdafunction.py index ea2c6575..2a27901c 100644 --- a/scar/providers/aws/lambdafunction.py +++ b/scar/providers/aws/lambdafunction.py @@ -106,9 +106,11 @@ def _get_function_code(self, zip_payload_path: str, supervisor_zip_path: str) -> FunctionPackager(self.resources_info, supervisor_zip_path).create_zip(zip_payload_path) if self.function.get('deployment').get('bucket', False): file_key = f"lambda/{self.function.get('name')}.zip" - S3(self.resources_info).upload_file(bucket=self.function.get('deployment').get('bucket'), - file_path=zip_payload_path, - file_key=file_key) + s3_client = S3(self.resources_info) + s3_client.create_bucket(self.function.get('deployment').get('bucket')) + s3_client.upload_file(bucket=self.function.get('deployment').get('bucket'), + file_path=zip_payload_path, + file_key=file_key) code = {"S3Bucket": self.function.get('deployment').get('bucket'), "S3Key": file_key} else: From 959841a1d740efe637f92e25fa4dca591d41d4f5 Mon Sep 17 00:00:00 2001 From: Sebas Risco Date: Tue, 7 Jan 2020 17:44:07 +0100 Subject: [PATCH 41/41] Bump --- scar/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scar/version.py b/scar/version.py index 6571f4ca..dc73d7d2 100644 --- a/scar/version.py +++ b/scar/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '3.2.12' +__version__ = '4.0.0'

;w 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%0DG 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