From 24296fe813e6c707269d33652f925b81097eb8bf Mon Sep 17 00:00:00 2001 From: Geoff Macartney Date: Fri, 12 May 2017 15:29:07 +0100 Subject: [PATCH] Temporarily restore simple-web-cluster. A file in it is still in use at https://github.com/apache/brooklyn-library/blob/master/qa/src/test/resources/java-web-app-and-db-with-policy.bom#L54 Will need to tidy that up so the file is no longer used and then this example can be deleted again. --- examples/pom.xml | 1 + examples/simple-web-cluster/.gitignore | 2 + examples/simple-web-cluster/README.txt | 59 +++++ examples/simple-web-cluster/pom.xml | 164 ++++++++++++++ .../resources/jmeter-test-plan.jmx | 143 ++++++++++++ .../src/main/assembly/assembly.xml | 74 +++++++ .../src/main/assembly/files/README.txt | 49 +++++ .../src/main/assembly/scripts/start.sh | 43 ++++ .../brooklyn/demo/NodeJsTodoApplication.java | 60 ++++++ .../brooklyn/demo/SingleWebServerExample.java | 66 ++++++ .../demo/WebClusterDatabaseExample.java | 122 +++++++++++ .../demo/WebClusterDatabaseExampleApp.java | 174 +++++++++++++++ .../brooklyn/demo/WebClusterExample.java | 95 ++++++++ .../src/main/resources/catalog.bom | 31 +++ .../src/main/resources/logback-custom.xml | 43 ++++ .../brooklyn/demo/glossy-3d-blue-web-icon.png | Bin 0 -> 46490 bytes .../brooklyn/demo/nodejs-riak-todo.yaml | 46 ++++ .../org/apache/brooklyn/demo/nodejs-todo.yaml | 53 +++++ .../resources/visitors-creation-script.sql | 41 ++++ ...sterDatabaseExampleAppIntegrationTest.java | 204 ++++++++++++++++++ 20 files changed, 1470 insertions(+) create mode 100644 examples/simple-web-cluster/.gitignore create mode 100644 examples/simple-web-cluster/README.txt create mode 100644 examples/simple-web-cluster/pom.xml create mode 100644 examples/simple-web-cluster/resources/jmeter-test-plan.jmx create mode 100644 examples/simple-web-cluster/src/main/assembly/assembly.xml create mode 100644 examples/simple-web-cluster/src/main/assembly/files/README.txt create mode 100755 examples/simple-web-cluster/src/main/assembly/scripts/start.sh create mode 100644 examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/NodeJsTodoApplication.java create mode 100644 examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/SingleWebServerExample.java create mode 100644 examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java create mode 100644 examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java create mode 100644 examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java create mode 100644 examples/simple-web-cluster/src/main/resources/catalog.bom create mode 100644 examples/simple-web-cluster/src/main/resources/logback-custom.xml create mode 100644 examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/glossy-3d-blue-web-icon.png create mode 100644 examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-riak-todo.yaml create mode 100644 examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-todo.yaml create mode 100644 examples/simple-web-cluster/src/main/resources/visitors-creation-script.sql create mode 100644 examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java diff --git a/examples/pom.xml b/examples/pom.xml index ac03e8e99..697a3f2f6 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -37,6 +37,7 @@ + simple-web-cluster webapps diff --git a/examples/simple-web-cluster/.gitignore b/examples/simple-web-cluster/.gitignore new file mode 100644 index 000000000..9a3d6d6d8 --- /dev/null +++ b/examples/simple-web-cluster/.gitignore @@ -0,0 +1,2 @@ +brooklyn-example-simple-web-cluster/ +brooklyn-example-simple-web-cluster.tar.gz diff --git a/examples/simple-web-cluster/README.txt b/examples/simple-web-cluster/README.txt new file mode 100644 index 000000000..598a4ce12 --- /dev/null +++ b/examples/simple-web-cluster/README.txt @@ -0,0 +1,59 @@ +Instructions for Running Examples +================================= + +The commands below assume that the `brooklyn` script is on your $PATH, this project has been built, +and you are in this directory. Adjust to taste for other configurations. + + export BROOKLYN_CLASSPATH=$(pwd)/target/classes + + # Three-tier: auto-scaling app-server cluster fronted by nginx, MySql backend wired up, on localhost + brooklyn launch --app org.apache.brooklyn.demo.WebClusterDatabaseExample --location localhost + +The above requires passwordless `ssh localhost` and requires `gcc` to build `nginx`. +You could instead target your favourite cloud, where this has been tried and tested: + + # Same three-tier, but in Amazon California, prompting for credentials + # (the location arg can be changed for any example, of course, and other clouds are available) + export JCLOUDS_AWS_EC2_IDENTITY=AKA50M30N3S1DFR0MAW55 + export JCLOUDS_AWS_EC2_CREDENTIAL=aT0Ps3cr3tC0D3wh1chAW5w1llG1V3y0uTOus333 + brooklyn launch --app brooklyn.demo.WebClusterDatabaseExample --location aws-ec2:us-west-1 + + +Other examples: + + # A very simple app: a single web-server + brooklyn launch --app org.apache.brooklyn.demo.SingleWebServerExample --location localhost + + # A simple app: just load-balancer and appservers + brooklyn launch --app org.apache.brooklyn.demo.WebClusterExample --location localhost + + # Three-tier example + brooklyn launch --app org.apache.brooklyn.demo.WebClusterDatabaseExample --location localhost + + +Redistributable embedded example: + + # To build a redistributable tar.gz with a start.sh script + # which invokes the `main` method in the example class to start + # (the redistributable will be at: target/brooklyn-*-bin.tar.gz ) + mvn clean assembly:assembly + +For more information please visit https://brooklyn.incubator.apache.org/. + +---- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. \ No newline at end of file diff --git a/examples/simple-web-cluster/pom.xml b/examples/simple-web-cluster/pom.xml new file mode 100644 index 000000000..ef5cd67da --- /dev/null +++ b/examples/simple-web-cluster/pom.xml @@ -0,0 +1,164 @@ + + + + 4.0.0 + jar + brooklyn-example-simple-web-cluster + Brooklyn Simple Web Cluster Example + + + + org.apache.brooklyn.example + brooklyn-examples-parent + 0.12.0-SNAPSHOT + ../pom.xml + + + + + org.apache.brooklyn + brooklyn-core + ${project.version} + + + org.apache.brooklyn + brooklyn-launcher + ${project.version} + + + org.apache.brooklyn + brooklyn-software-webapp + ${project.version} + + + org.apache.brooklyn + brooklyn-software-database + ${project.version} + + + org.apache.brooklyn + brooklyn-software-nosql + ${project.version} + + + org.apache.brooklyn + brooklyn-logback-xml + ${project.version} + true + + + com.google.guava + guava + + + org.slf4j + slf4j-api + + + + org.apache.brooklyn + brooklyn-core + ${project.version} + tests + test + + + org.apache.brooklyn + brooklyn-test-support + ${project.version} + test + + + org.testng + testng + test + + + + + + + maven-dependency-plugin + + + + copy + process-classes + + copy + + + + + + org.apache.brooklyn.example + brooklyn-example-hello-world-webapp + ${project.version} + war + true + target/classes + hello-world-webapp.war + + + org.apache.brooklyn.example + brooklyn-example-hello-world-sql-webapp + ${project.version} + war + true + target/classes + hello-world-sql-webapp.war + + + + + + + + maven-clean-plugin + + + + ${project.basedir} + + ${project.artifactId}/ + brooklyn*.log + brooklyn*.log.* + stacktrace.log + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + src/main/assembly/assembly.xml + + + + + + + diff --git a/examples/simple-web-cluster/resources/jmeter-test-plan.jmx b/examples/simple-web-cluster/resources/jmeter-test-plan.jmx new file mode 100644 index 000000000..0eb1741e1 --- /dev/null +++ b/examples/simple-web-cluster/resources/jmeter-test-plan.jmx @@ -0,0 +1,143 @@ + + + + + + + false + false + + + + + + + + continue + + false + -1 + + 8 + 1 + 1326116677000 + 1326116677000 + false + + + + + + 20 + + + + + + + localhost + 8000 + + + + + + GET + true + false + true + false + false + + + + + + false + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + true + false + false + false + false + false + 0 + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + true + false + false + false + false + false + 0 + true + + + + + + + + diff --git a/examples/simple-web-cluster/src/main/assembly/assembly.xml b/examples/simple-web-cluster/src/main/assembly/assembly.xml new file mode 100644 index 000000000..cbe7a0829 --- /dev/null +++ b/examples/simple-web-cluster/src/main/assembly/assembly.xml @@ -0,0 +1,74 @@ + + + + bin + + + tar.gz + + + + + + + false + lib + false + + + + + + + src/main/assembly/scripts + + 0755 + + * + + + + + src/main/assembly/files + + + * + + + + + target + + + *.jar + + + + + diff --git a/examples/simple-web-cluster/src/main/assembly/files/README.txt b/examples/simple-web-cluster/src/main/assembly/files/README.txt new file mode 100644 index 000000000..cf49ea596 --- /dev/null +++ b/examples/simple-web-cluster/src/main/assembly/files/README.txt @@ -0,0 +1,49 @@ +Brooklyn Example +================ + +To use, configure your cloud credentials then run ./start.sh in this directory. +You can then access the management context in your browser, typically on localhost:8081. + + +### Cloud Credentials + +To run, you'll need to specify credentials for your preferred cloud. This can be done +in `~/.brooklyn/brooklyn.properties`: + + brooklyn.jclouds.aws-ec2.identity=AKXXXXXXXXXXXXXXXXXX + brooklyn.jclouds.aws-ec2.credential=secret01xxxxxxxxxxxxxxxxxxxxxxxxxxx + +Alternatively these can be set as shell environment parameters or JVM system properties. + +Many other clouds are supported also, as well as pre-existing machines ("bring your own nodes"), +custom endpoints for private clouds, and specifying custom keys and passphrases. +For more information see: + + https://github.com/brooklyncentral/brooklyn/blob/master/docs/use/guide/defining-applications/common-usage.md#off-the-shelf-locations + + +### Run + +Usage: + + ./start.sh [--port 8081+] location + +Where location might be `localhost` (the defaul), or `aws-ec2:us-east-1`, `openstack:endpoint`, etc. + +---- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. \ No newline at end of file diff --git a/examples/simple-web-cluster/src/main/assembly/scripts/start.sh b/examples/simple-web-cluster/src/main/assembly/scripts/start.sh new file mode 100755 index 000000000..ec4f1105c --- /dev/null +++ b/examples/simple-web-cluster/src/main/assembly/scripts/start.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +if [ -z "$BROOKLYN_APP_CLASS" ] ; then + BROOKLYN_APP_CLASS=org.apache.brooklyn.demo.WebClusterDatabaseExample +fi + +if [ -z "$JAVA" ] ; then + JAVA=`which java` +fi +if [ ! -z "$JAVA_HOME" ] ; then + JAVA=$JAVA_HOME/bin/java +else + JAVA=`which java` +fi +if [ ! -x "$JAVA" ] ; then + echo Cannot find java. Set JAVA_HOME or add java to path. + exit 1 +fi + +if [[ ! `ls *.jar 2> /dev/null` || ! `ls lib/*.jar 2> /dev/null` ]] ; then + echo Command must be run from the directory where it is installed. + exit 1 +fi + +$JAVA -Xms256m -Xmx1024m -XX:MaxPermSize=1024m -classpath "*:lib/*" $BROOKLYN_APP_CLASS "$@" diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/NodeJsTodoApplication.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/NodeJsTodoApplication.java new file mode 100644 index 000000000..086f39091 --- /dev/null +++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/NodeJsTodoApplication.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import static org.apache.brooklyn.core.sensor.DependentConfiguration.attributeWhenReady; + +import org.apache.brooklyn.api.catalog.Catalog; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.core.entity.AbstractApplication; +import org.apache.brooklyn.core.entity.Attributes; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.entity.trait.Startable; +import org.apache.brooklyn.core.sensor.DependentConfiguration; +import org.apache.brooklyn.entity.nosql.redis.RedisStore; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.entity.webapp.nodejs.NodeJsWebAppService; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Node.JS Todo Application + */ +@Catalog(name="NodeJS Todo", + description="Node.js is a cross-platform runtime environment for server-side and networking applications. Node.js applications are written in JavaScript", + iconUrl="classpath://nodejs-logo.png") +public class NodeJsTodoApplication extends AbstractApplication implements StartableApplication { + + @Override + public void initApp() { + RedisStore redis = addChild(EntitySpec.create(RedisStore.class)); + + addChild(EntitySpec.create(NodeJsWebAppService.class) + .configure(NodeJsWebAppService.APP_GIT_REPOSITORY_URL, "https://github.com/grkvlt/nodejs-todo/") + .configure(NodeJsWebAppService.APP_FILE, "server.js") + .configure(NodeJsWebAppService.APP_NAME, "nodejs-todo") + .configure(NodeJsWebAppService.NODE_PACKAGE_LIST, ImmutableList.of("express", "ejs", "jasmine-node", "underscore", "method-override", "cookie-parser", "express-session", "body-parser", "cookie-session", "redis", "redis-url", "connect")) + .configure(SoftwareProcess.SHELL_ENVIRONMENT, ImmutableMap.of( + "REDISTOGO_URL", DependentConfiguration.formatString("redis://%s:%d/", + attributeWhenReady(redis, Attributes.HOSTNAME), attributeWhenReady(redis, RedisStore.REDIS_PORT)))) + .configure(SoftwareProcess.LAUNCH_LATCH, attributeWhenReady(redis, Startable.SERVICE_UP))); + } + +} diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/SingleWebServerExample.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/SingleWebServerExample.java new file mode 100644 index 000000000..cec7a7e21 --- /dev/null +++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/SingleWebServerExample.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.core.entity.AbstractApplication; +import org.apache.brooklyn.core.entity.Attributes; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.location.PortRanges; +import org.apache.brooklyn.entity.webapp.JavaWebAppService; +import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server; +import org.apache.brooklyn.launcher.BrooklynLauncher; +import org.apache.brooklyn.util.CommandLineUtil; + +import com.google.common.collect.Lists; + +/** This example starts one web app on 8080, waits for a keypress, then stops it. */ +public class SingleWebServerExample extends AbstractApplication { + + public static final Logger LOG = LoggerFactory.getLogger(SingleWebServerExample.class); + + private static final String WAR_PATH = "classpath://hello-world-webapp.war"; + + @Override + public void initApp() { + addChild(EntitySpec.create(JBoss7Server.class) + .configure(JavaWebAppService.ROOT_WAR, WAR_PATH) + .configure(Attributes.HTTP_PORT, PortRanges.fromString("8080+"))); + } + + // Shows how to use ApplicationBuilder without sub-classing, but for CLI usage one should sub-class + public static void main(String[] argv) throws Exception { + List args = Lists.newArrayList(argv); + String port = CommandLineUtil.getCommandLineOption(args, "--port", "8081+"); + String location = CommandLineUtil.getCommandLineOption(args, "--location", "localhost"); + + BrooklynLauncher launcher = BrooklynLauncher.newInstance() + .application(EntitySpec.create(StartableApplication.class, SingleWebServerExample.class).displayName("Brooklyn WebApp example")) + .webconsolePort(port) + .location(location) + .start(); + + Entities.dumpInfo(launcher.getApplications()); + } +} diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java new file mode 100644 index 000000000..601537acd --- /dev/null +++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import static org.apache.brooklyn.core.sensor.DependentConfiguration.attributeWhenReady; +import static org.apache.brooklyn.core.sensor.DependentConfiguration.formatString; +import static org.apache.brooklyn.entity.java.JavaEntityMethods.javaSysProp; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.entity.database.mysql.MySqlNode; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.core.entity.AbstractApplication; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.location.PortRanges; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.enricher.stock.Enrichers; +import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.JavaWebAppService; +import org.apache.brooklyn.entity.webapp.WebAppService; +import org.apache.brooklyn.entity.webapp.WebAppServiceConstants; +import org.apache.brooklyn.launcher.BrooklynLauncher; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.policy.enricher.HttpLatencyDetector; +import org.apache.brooklyn.util.CommandLineUtil; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +/** + * Launches a 3-tier app with nginx, clustered jboss, and mysql. + **/ +public class WebClusterDatabaseExample extends AbstractApplication { + + public static final Logger LOG = LoggerFactory.getLogger(WebClusterDatabaseExample.class); + + public static final String WAR_PATH = "classpath://hello-world-sql-webapp.war"; + + public static final String DB_SETUP_SQL_URL = "classpath://visitors-creation-script.sql"; + + public static final String DB_TABLE = "visitors"; + public static final String DB_USERNAME = "brooklyn"; + public static final String DB_PASSWORD = "br00k11n"; + + public static final AttributeSensor APPSERVERS_COUNT = Sensors.newIntegerSensor( + "appservers.count", "Number of app servers deployed"); + + @Override + public void initApp() { + MySqlNode mysql = addChild(EntitySpec.create(MySqlNode.class) + .configure("creationScriptUrl", DB_SETUP_SQL_URL)); + + ControlledDynamicWebAppCluster web = addChild(EntitySpec.create(ControlledDynamicWebAppCluster.class) + .configure(WebAppService.HTTP_PORT, PortRanges.fromString("8080+")) + .configure(JavaWebAppService.ROOT_WAR, WAR_PATH) + .configure(javaSysProp("brooklyn.example.db.url"), + formatString("jdbc:%s%s?user=%s\\&password=%s", + attributeWhenReady(mysql, MySqlNode.DATASTORE_URL), + DB_TABLE, DB_USERNAME, DB_PASSWORD)) ); + + web.enrichers().add(HttpLatencyDetector.builder(). + url(ControlledDynamicWebAppCluster.ROOT_URL). + rollup(10, TimeUnit.SECONDS). + build()); + + // simple scaling policy + web.getCluster().policies().add(AutoScalerPolicy.builder(). + metric(DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE). + metricRange(10, 100). + sizeRange(1, 5). + build()); + + // expose some KPI's + enrichers().add(Enrichers.builder() + .propagating(WebAppServiceConstants.ROOT_URL, + DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW, + HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_IN_WINDOW) + .from(web) + .build()); + + enrichers().add(Enrichers.builder() + .propagating(ImmutableMap.of(DynamicWebAppCluster.GROUP_SIZE, APPSERVERS_COUNT)) + .from(web) + .build()); + } + + public static void main(String[] argv) { + List args = Lists.newArrayList(argv); + String port = CommandLineUtil.getCommandLineOption(args, "--port", "8081+"); + String location = CommandLineUtil.getCommandLineOption(args, "--location", "localhost"); + + BrooklynLauncher launcher = BrooklynLauncher.newInstance() + .application(EntitySpec.create(StartableApplication.class, WebClusterDatabaseExample.class).displayName("Brooklyn WebApp Cluster with Database example")) + .webconsolePort(port) + .location(location) + .start(); + + Entities.dumpInfo(launcher.getApplications()); + } +} diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java new file mode 100644 index 000000000..5c58fee8b --- /dev/null +++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import static org.apache.brooklyn.core.sensor.DependentConfiguration.attributeWhenReady; +import static org.apache.brooklyn.core.sensor.DependentConfiguration.formatString; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.catalog.Catalog; +import org.apache.brooklyn.api.catalog.CatalogConfig; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.entity.AbstractApplication; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.location.PortRanges; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.enricher.stock.Enrichers; +import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.JavaWebAppService; +import org.apache.brooklyn.entity.webapp.WebAppService; +import org.apache.brooklyn.entity.webapp.WebAppServiceConstants; +import org.apache.brooklyn.entity.database.mysql.MySqlNode; +import org.apache.brooklyn.entity.group.DynamicCluster; +import org.apache.brooklyn.entity.java.JavaEntityMethods; +import org.apache.brooklyn.launcher.BrooklynLauncher; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.policy.enricher.HttpLatencyDetector; +import org.apache.brooklyn.util.CommandLineUtil; +import org.apache.brooklyn.util.core.BrooklynMavenArtifacts; +import org.apache.brooklyn.util.core.ResourceUtils; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; + +/** + * Launches a 3-tier app with nginx, clustered jboss, and mysql. + *

+ * Includes some advanced features such as KPI / derived sensors, + * and annotations for use in a catalog. + *

+ * This variant also increases minimum size to 2. + * Note the policy min size must have the same value, + * otherwise it fights with cluster set up trying to reduce the cluster size! + **/ +@Catalog(name="Elastic Java Web + DB", + description="Deploys a WAR to a load-balanced elastic Java AppServer cluster, " + + "with an auto-scaling policy, " + + "wired to a database initialized with the provided SQL; " + + "defaults to a 'Hello World' chatroom app.", + iconUrl="classpath://brooklyn/demo/glossy-3d-blue-web-icon.png") +public class WebClusterDatabaseExampleApp extends AbstractApplication implements StartableApplication { + + public static final Logger LOG = LoggerFactory.getLogger(WebClusterDatabaseExampleApp.class); + + public static final String DEFAULT_LOCATION = "localhost"; + + public static final String DEFAULT_WAR_PATH = ResourceUtils.create(WebClusterDatabaseExampleApp.class) + // take this war, from the classpath, or via maven if not on the classpath + .firstAvailableUrl( + "classpath://hello-world-sql-webapp.war", + BrooklynMavenArtifacts.localUrl("example", "brooklyn-example-hello-world-sql-webapp", "war")) + .or("classpath://hello-world-sql-webapp.war"); + + @CatalogConfig(label="WAR (URL)", priority=2) + public static final ConfigKey WAR_PATH = ConfigKeys.newConfigKey( + "app.war", "URL to the application archive which should be deployed", + DEFAULT_WAR_PATH); + + // TODO to expose in catalog we need to let the keystore url be specified (not hard) + // and also confirm that this works for nginx (might be a bit fiddly); + // booleans in the gui are working (With checkbox) + @CatalogConfig(label="HTTPS") + public static final ConfigKey USE_HTTPS = ConfigKeys.newConfigKey( + "app.https", "Whether the application should use HTTPS only or just HTTP only (default)", false); + + public static final String DEFAULT_DB_SETUP_SQL_URL = "classpath://visitors-creation-script.sql"; + + @CatalogConfig(label="DB Setup SQL (URL)", priority=1) + public static final ConfigKey DB_SETUP_SQL_URL = ConfigKeys.newConfigKey( + "app.db_sql", "URL to the SQL script to set up the database", + DEFAULT_DB_SETUP_SQL_URL); + + public static final String DB_TABLE = "visitors"; + public static final String DB_USERNAME = "brooklyn"; + public static final String DB_PASSWORD = "br00k11n"; + + public static final AttributeSensor APPSERVERS_COUNT = Sensors.newIntegerSensor( + "appservers.count", "Number of app servers deployed"); + public static final AttributeSensor REQUESTS_PER_SECOND_IN_WINDOW = + WebAppServiceConstants.REQUESTS_PER_SECOND_IN_WINDOW; + public static final AttributeSensor ROOT_URL = WebAppServiceConstants.ROOT_URL; + + @Override + public void initApp() { + MySqlNode mysql = addChild( + EntitySpec.create(MySqlNode.class) + .configure(MySqlNode.CREATION_SCRIPT_URL, Entities.getRequiredUrlConfig(this, DB_SETUP_SQL_URL))); + + ControlledDynamicWebAppCluster web = addChild( + EntitySpec.create(ControlledDynamicWebAppCluster.class) + .configure(WebAppService.HTTP_PORT, PortRanges.fromString("8080+")) + // to specify a diferrent appserver: +// .configure(ControlledDynamicWebAppCluster.MEMBER_SPEC, EntitySpec.create(TomcatServer.class)) + .configure(JavaWebAppService.ROOT_WAR, Entities.getRequiredUrlConfig(this, WAR_PATH)) + .configure(JavaEntityMethods.javaSysProp("brooklyn.example.db.url"), + formatString("jdbc:%s%s?user=%s\\&password=%s", + attributeWhenReady(mysql, MySqlNode.DATASTORE_URL), DB_TABLE, DB_USERNAME, DB_PASSWORD)) + .configure(DynamicCluster.INITIAL_SIZE, 2) + .configure(WebAppService.ENABLED_PROTOCOLS, ImmutableSet.of(getConfig(USE_HTTPS) ? "https" : "http")) ); + + web.enrichers().add(HttpLatencyDetector.builder() + .url(ROOT_URL) + .rollup(10, TimeUnit.SECONDS) + .build()); + + web.getCluster().policies().add(AutoScalerPolicy.builder() + .metric(DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE) + .metricRange(10, 100) + .sizeRange(2, 5) + .build()); + + enrichers().add(Enrichers.builder() + .propagating(WebAppServiceConstants.ROOT_URL, + DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW, + HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_IN_WINDOW) + .from(web) + .build()); + + enrichers().add(Enrichers.builder() + .propagating(ImmutableMap.of(DynamicWebAppCluster.GROUP_SIZE, APPSERVERS_COUNT)) + .from(web) + .build()); + } + + public static void main(String[] argv) { + List args = Lists.newArrayList(argv); + String port = CommandLineUtil.getCommandLineOption(args, "--port", "8081+"); + String location = CommandLineUtil.getCommandLineOption(args, "--location", DEFAULT_LOCATION); + + BrooklynLauncher launcher = BrooklynLauncher.newInstance() + .application(EntitySpec.create(StartableApplication.class, WebClusterDatabaseExampleApp.class) + .displayName("Brooklyn WebApp Cluster with Database example")) + .webconsolePort(port) + .location(location) + .start(); + + Entities.dumpInfo(launcher.getApplications()); + } +} diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java new file mode 100644 index 000000000..e5a1ae23d --- /dev/null +++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.core.entity.AbstractApplication; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.entity.proxy.nginx.NginxController; +import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server; +import org.apache.brooklyn.launcher.BrooklynLauncher; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.util.CommandLineUtil; + +import com.google.common.collect.Lists; + +/** + * Launches a clustered and load-balanced set of web servers. + * Demonstrates syntax, so many of the options used here are the defaults. + * (So the class could be much simpler, as in WebClusterExampleAlt.) + *

+ * Requires: + * -Xmx512m -Xms128m -XX:MaxPermSize=256m + * and brooklyn-all jar, and this jar or classes dir, on classpath. + **/ +public class WebClusterExample extends AbstractApplication { + public static final Logger LOG = LoggerFactory.getLogger(WebClusterExample.class); + + static BrooklynProperties config = BrooklynProperties.Factory.newDefault(); + + public static final String DEFAULT_LOCATION = "localhost"; + + public static final String WAR_PATH = "classpath://hello-world-webapp.war"; + + private NginxController nginxController; + private ControlledDynamicWebAppCluster web; + + @Override + public void initApp() { + nginxController = addChild(EntitySpec.create(NginxController.class) + //.configure("domain", "webclusterexample.brooklyn.local") + .configure("port", "8000+")); + + web = addChild(EntitySpec.create(ControlledDynamicWebAppCluster.class) + .displayName("WebApp cluster") + .configure(ControlledDynamicWebAppCluster.CONTROLLER, nginxController) + .configure(ControlledDynamicWebAppCluster.INITIAL_SIZE, 1) + .configure(ControlledDynamicWebAppCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class) + .configure("httpPort", "8080+") + .configure("war", WAR_PATH))); + + web.getCluster().policies().add(AutoScalerPolicy.builder() + .metric(DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE) + .sizeRange(1, 5) + .metricRange(10, 100) + .build()); + } + + public static void main(String[] argv) { + List args = Lists.newArrayList(argv); + String port = CommandLineUtil.getCommandLineOption(args, "--port", "8081+"); + String location = CommandLineUtil.getCommandLineOption(args, "--location", DEFAULT_LOCATION); + + // TODO Want to parse, to handle multiple locations + BrooklynLauncher launcher = BrooklynLauncher.newInstance() + .application(EntitySpec.create(WebClusterExample.class).displayName("Brooklyn WebApp Cluster example")) + .webconsolePort(port) + .location(location) + .start(); + + Entities.dumpInfo(launcher.getApplications()); + } +} diff --git a/examples/simple-web-cluster/src/main/resources/catalog.bom b/examples/simple-web-cluster/src/main/resources/catalog.bom new file mode 100644 index 000000000..4d5cd8cc0 --- /dev/null +++ b/examples/simple-web-cluster/src/main/resources/catalog.bom @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +brooklyn.catalog: + version: "0.12.0-SNAPSHOT" # BROOKLYN_VERSION + itemType: template + items: + - id: org.apache.brooklyn.demo.NodeJsTodoApplication + item: + services: + - type: org.apache.brooklyn.demo.NodeJsTodoApplication + name: NodeJS Todo + - id: org.apache.brooklyn.demo.WebClusterDatabaseExampleApp + item: + services: + - type: org.apache.brooklyn.demo.WebClusterDatabaseExampleApp + name: Elastic Java Web + DB diff --git a/examples/simple-web-cluster/src/main/resources/logback-custom.xml b/examples/simple-web-cluster/src/main/resources/logback-custom.xml new file mode 100644 index 000000000..02a8a82d9 --- /dev/null +++ b/examples/simple-web-cluster/src/main/resources/logback-custom.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/glossy-3d-blue-web-icon.png b/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/glossy-3d-blue-web-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..542a1debfc2172f0002fb814d19be4e9ecf1386b GIT binary patch literal 46490 zcmdRVhf`DC7wt{x9i%D<_^I@&C>??bf*>F$y%$00y^~M`1f)onVvr_E?;S$#gx(=k z0qLDUNM3$#-aqj&b0?WQ_h!yMXP>p#-fN#PS{kpY$nTK@06?Xx@=_ZBK=_{^fRq^j zW$ae?0RU<&R9`B*^O@fZBr#{3s<{xaGBK&#Ty|XKFJN&nCT)2HA^-R&o;Zw9oAYz( zGCP_c%sWYV^5PCSnD7Nv@GNYnyQhAka?`2*-}omzqb>Qm;l;cAf6^@P9tIx|jg)?{ zwf!5gCB6B>KAV$>w~vm$=cin72tyucga$dF6(7RZt_{FK7&b^#zySX4t8xYa|Da?@ zJ^=jhaRDNLM@1ploELy7kh>A8fdF#t_=o!cAHUl9?uXABS#ERlBK&p!ki&EP4^O_E zzkfe9WG*5xczCIKf)2=iGr{(HSD{)c*ORV9v6|5Cc;bPb&Ak+x_B$^k`to-4c@4eC z8WX1-tHs7E<+C&n)gRr-SXfw?8E<^fM*VC1u&P2|K(TiqGw6C}`e!Q_(M}r!up&aI z?Nd+X4e~?Vfnp}E+qY-fC}tK97fvt063lB!++=6)3U>5D>|Wrv<>fbO znSz9*bBcj)D8^050TEbO(vRa5~}p%f|QlZb0W=9elUfEX6PruU>s zL+1~5))#nB(6B83o(GjeTcVJFDAdGF z!y3saBSA{a`GtjDGFsD|bAsRRqC`Wbz@b-7E^`!X31Fak@I{n-VNQ;v>Bo;dw*6&h zFJH??!LncFA4b<4Mu%K768h<21K|C=pEXa?;5+2zfD%T{NfFD|)mzfx=%J-vj%I48!(D^p-ALf{Y*3dqq2euQr@0jSZCDL=7;=i6@^KQ&Lhg>~4fC37VFZIk4>b_d=p8OcaN3TjO%hGbudNn zW~M3VVLJ!qB9c38vBs2!L_Xc6{sRu z*NwvIU~jKEc@}ZteFkps%>#fyh7afBj`L@t+SktXfVz#Y8PRXQs_Wb1;~O6#=bhWo znKa}YRNk}6e^POJXY23j_oMF)`-%u# z&Xr%DhzN5;9IfE07C>i+2{NlY43yqwqzm7X{hgc+-U| zgKn>h^3LQri=a=`_IFyb?N^wmZ{zz}<-3aDKhIt8b0lalna%OHXWTP<_8ehJ0&&*tE~R+zlRq4Vu0~ z`#{$Kozi{jPBawd$txNXRY}S>82wg>!u_$|fZy5Hz4I<`^TL;x;Eg+b9~~WUxf%#B zneHWcq=Eq->)q)R#hqY`9Qr0T9>p=;w11X-?O)e2o_c4lCxyo>-B5arPV*#?5~lvU zEBpbXZv|M%4Beq;$a;I?5MLOJUhbscDloIv#%c~?G>*9w(6tf%}s*dFYw@EOSTB~xd6>ruS~Oc0^}3SFu5hyvW=RC3o5JqC4X95ayq{J5 z(u(cTB7`#y!j)LdSY6B$VlidIVrrsM`ogu&{qXR7hy)A{$Xwy%g+`?zZuj^1QNE~~ z<+@vQc=F{pwqpQ76R9xJHgtH2Yc4FbYejCYx16~125Or4+#Ja+O%MG-naqR)k{I^? zDafy}B|oeZl>2o{NXWRMrk4`tn_a>))N{8XIfLZ95q;SbiA`Wvv|g47j^MVV3@73? z%5??TXjqSToVY7(s3}_!F^Q%C%9JHtg%-($e8R3K6QM1&XePXo2A1GBPY;g(_qj?N z*6Y*aX6DUEVxR!CQ0FY4m4w47{g+@MfZMOZ1im@llnknKto@LH$+}4||5bYT_))YP znZ2+;i>6kfVN`#kUINbey>6Fa>y@@($l;q^Pqot)K|S8B52jl2t}tp#qG3X?0BlaR zSh=3?4&jE^^TbuSO$||QxSqvcqykAdsh=7>AL8wuugV7JE6|U(=iSlFWxlM!(-z#X z1OSoEvXTys*8`GnP8AJ%?HijMG^*m_x&@pn(7jM9VLBcCO=FpqtXufk6p}) z?o4Y`*!f%KQTQtqO~=V4?$hS%-cMbHTc>N`1a)^>JBR&^Vu&p`#hRFswB8; zDTd?yu+`l_s?rNFTLro2h-|1uVou48L3&l!g=Dbh8k%NqX>PFbH5ID~VY%Z6L*w(h3*nko9< z>K+tNrDmCGl{Y#XeOj>G84^n;CHnO*`I+v+ zcoIUa+5zIdT8e-Hj;VCkg8vm8epXNwRwNW>OOOF*^}UXwZLHDzpu@)R0;rVx=Y?~D zurrAA`5U++{tCwPO3D(x&T@r(8CxO6+qD~GCey6Ys^PXlPp%%Lwj%PrS?10AdgdQ( z35|{g9gSL6!6EPNoeSRoZ{_aSx!@O@_gtPs%HCWbbyakV*kD{tHIH-;6g4>^lrSp6 zpcqjQIM~toc;LR5TNeo6#mN}nrYAvv^7=yMHn*60G~a&JbCdN^KNfdHX|}qk^GM&Y z!?Q7RaE@RYM`y%4^Q-J9riS)u*PSKF`#+n;FY~t9r(SBp|J_FXWZfw*K7cxJm=o$! ze=s*f>4QDH!Y}UIJ=9W>7!Y0p5fK)LoBs==OH+re7-zH+k@>rkyvqV3tzYIAe>ytE z@4lJLfRp}L?*=O#k^>j}3*vGKDwxK?!fka)MW;F#YT+lQ`p?5Iv5tUFdMZ*m_0Tk0fMBHkUG{uh{+5-9%p{E67?TQle4io zyGa&i%7(x|8j6aF{GfJ5?ktb-rCaDYwY)Eq0I=Us7{Gne;yJT7GPJes+^;f<3j7e-5IG2u2fj7 z)xl&%n!NoBZ>Fivz+GPU?0@q5>th5Y)O$|O&Nzl(#6+M^7qz{wCIKww3a*}n)2+lN zHrL$x{fs*RZ`<$dmpV!hpr*g1&Ny@y+$VYIhdk5|EWi*4BaZ%&$8c}juK%S*3*L(cAiA&)H#h zTE2+sFS*Mlr1;kxw&3*-nlF-La?eNz&p5MZ?wix6-63tBotr~{$K$W_ze^(NmrH%U^92v65-FDtRwVE z1=pK^D!T5dQiGdz)5U02+=(HqeR)Lu=I3CFzwDn^75Q$4R{Y$Es?W?DfB)VQKEoI* zsjtYgG6W#Oz)~OSTO2%b69*QB(JpLJU7vHW9e(9HoJWNvmpOJXhd=K{a(}+8kZI)w z2U&$vb#%QA^^q2|L9Y_s8$R*o9&(ajtn9RB&qvZlBL7;E>mK6w(J)mfBf-kdTj{4Q z8b76Z>Py;!jHP2Nb|BfoK+A`c;o8HGT|%sc>(1@s$BaIOkE?w(+h*|UExB)2lba-B*^O}sU!YqtBBg8psTxdX*m_uJ>T zO-9cum`JO={TY|4kY@oJj{fZk3nEhn_eJotu(b0Bg+F&&no7S1c^K*k^%Dw=5EAml zuo-?yjQt*UoEvc_8NWMB>SIr+uN7|`99~;ntND%p2p;OgK+@d21wgKEdlRSH5({x+ zONWOgE8M*mdo}aRwYxw{TQlAHaMpj_y~(EfL7~5{F)X6hu5UCG=8hiK^8gb%XA{8mky`OG2Fi5!B#| zvbyHair-=WrE-t*D3$K2inwpx?$dnMhxsdrypt!7qhua}N z9i3B~lN;#T-)0=>ii!%Pipm!WcqPTr((xpp2;fA%l!i5CyC9s@^F5w*hjUR<#-h~hHvDtm1Vdo1J=JQt;EV9*$$LBWu!ZQ=4I?%g!y^{E+L0 z;=P&e?_fmY;EaeG2oVqsXY8 z#-r#k?`%=mwCWuL8HHJmg(s^^*B}4 zS8UHRZ_myqG&MQWxo`KAFA0IZLR@d;HR{DA{MD_2H+;Dm8oNGgUn6sddavK$*R;Qz z@$G=Mng<21sY;i0Otm5ZBAi+UlHT#QSs1xyjqkS08y(Mu@|^!?|7u`K9fI@E zx~ACuJ+*?UNzgfAypdR??4&-ryOd;VO{@O#I13qh4KQ;8_ecw8e&js34-^aJWRVq@ zEyewGh3a z9-P043>icMcyx=`%)q2|%su#7#-W+p63tr>)@er+cMlmYK_7nXWSsGwh*|^a#wGj- z2R9w<+2f-O;fL9;KkrBm-l3=$PJSKewA%4G;&#$1LKYbPdhEPVyT9;LYkMJYFTr@S zO&ss?Cso0+G?*>V>X-D!=&a#wR_XQ<=KdVUku8;*1DV7 ztNI7U7e5Sb1?%goQYa|hKwtLB5hc=2w0%gR`57p-n^}RvX7yAQ(7}3ekSM-Fdvc1% zPNO_!ZZQJBQ)h_mcSAJ`7glD9MT)l#YihLS=J9um&0Ty&g(R#%A2isP%C-=L;K{~@ zhK5{x)O521L1ibAIHTmwdw~9m^VUe#Q@hhve4YHg@KwL=n(FY+P$t zdW)bNi!;n9$m#Ymh51eBPm#as_rf0}D?M4+dUNS}8`znj-RQRa`k*-lKG@ifIB9nF z1u5VhFKQ(cf2>2v3)tv%QGIWwsCrL!qskDl3eZIEQOE<9rHi7ry0*d5Peg7uB$YX- zh;+xB_}7mdcaZ?hmQ>pk{@5h+H+}eeM-{VWU-n6}diJkknBnZ!T#RCu6g*lvm&3Hy z!C>jlX5nS#D$#M-T|vR?(?(=t>Mp;ut^F zQ#WT|h0C=X5@OGum??g!rxF}5Q#$wjJUrmpb0^Aa&Yp_48eyjFPQ^g#LBnO3F3kxo zy85Z1!O2al0+cUmhW~u)nK4hWG!84M-YBSbOw%6wITo}DP7Ak(6Yz!e^JgZZ)h1&n0(VibnzzT>VQ>jCIQ-_I$*-(5~d1wot(M2*Zm?&*Rg}5ajPFsE?{Ix*bJMJb^i^XZSEB8}D9ym>Dp>ifBV(9f&!HCs>OY@>j{5`!TAx^d~A zaT5+sZ>bsKt=mWb}6fzmk+xim(G76_EMKqI5 z1)Q0QZ`M%+ihU+r#L5tasC!?-k9h)>uWuW)gO~MT*X)d>9zjJnp3a^CwG7LT?93Pf zmIDcZo;i6Y2OIS_BmHFe%uEz?Q73m_VX6IcTq=vi(Jw({l8PPiiD)aD}7l`n$Du59pmS=V##lM zopI_M_7?6f$a<4g&y6_246TtgbX3*w?#L5FBup`;b^pOqe!XVZn0@V2{3P9tHC zNiP&>{R}E!#4GpC)Qq2W{2#$0fu9={`L~6GXyO@ZLV%tRQ-8L>8-R2i6u0u{e{0)m z3)Q6yx}(2C}t?c`lyxHI=#;XT2dt!A$+ z(4OQdA3s><8<7a0H4Se7|M#YkrG2kI_qaQH#N~}rfxK{FKdGI-ililo!=%!>-(p4^ z<3b1nK!{5`dWg@}=3r&fSlOj_x=Ws@^|BTC;7p(EEhP{l^G`S=@PRZzwsGiPt3RTj zw7#g^ejhkYO`X8!?T$6i;IbLXbWqtpWoe*lN&w{rd8v8XE>|mEn7UBoeOUcc6vXwO z*agiC2Aq69(SI`3&Kc5_V^~(aTdHVas1WaG)xrM!ao7P15+DzgxSfM~BfdXtJl)2Pl4k}SN|qu}7#qopXG;4p_>KW< zF&1tMV8RGM0%=d!9w%g%h{WQ`l4i;YT%#&&^V@_Wh<&j2`+sFdjxck=fAzi-{zrUm z3u{J8a+&RUe}Ga68$PJ-M4}V0EfiaMS4yVujb62>fqhebuiHTIPd!kGt>zi<6G=>J zNmxKndp6CyJ;g{anroukTitJz(lJ~Hji8f^@ouXa}iOnNEefvq4y+Nq$?Yd z$e0n}oieAY6LABGCV};64iB5p*)17&QrUC?MEK3YV#BHB^AhG3fqhl9KP$T1iQQlS zpc!W%bAeIjUg}N$!5g4|bxRId(`ng!q>F`%F_fn3eLt{@`$L~E?f_`LLmi*X0@+IF zDLK0#(7g;7rOSQjqOP^dc=YSnXx>0z|KXOLybd-yV4s;6LQP%(zdVDB zHjXWK_LK-@|8Vqh1W z;ek#}(}$l{tO&1ULcdotVG?aR1fHo(r!!MLzO%^4qHPf3Z0yBe5W_L%DCKiI*RRKn zSOmC^eKLndecbi*X2c_jx|Z*M_1Sfs1po@{{&LIBBg3le-Gqi>sIx5gETb<(ybj|i z$qu!k=U5vI3{pO|If^3z*?v?n>_>)}ZqULI7P=T{4KP#-t+H_$AotyqVs zFctUDH;BvNP5T}T44dKi51H^xyc&5s~9Iq%iqzIS2Ry(9lM3xJ%K6nO4|*AvW-*Sen_mP<7| z{?FdAB8a>Dsitaw*@vyH!E66ps7W(vtJkRz2165gTJG^uzxRF^RXVp2Io9O>K5MF*u3Kj!28SyiyFVe{Rz)6|{Sbbd z9TIWP`Crz-?&lX?o7p>rWK{b;BbSlUCiv2uxc%-XX?u*re}4eZ{t|f-nDvW#@Dqqc z^&di1+sVT;F()551Yty<9TJQeQv8&K1mDZ=%HdwR-Cq%y}mwq1_z?T3%Q9J_R2SaAUTv9z2L9gvApWWMhhG4$j>bY9-6B(m48+F& zk3V%jIXq0lz3>7|#e7*h9<)gmSoDSYmc9-8(TFW>LiD~rDWbZt_%_A9cd*m4oP-io z3W`pGX1>D;$S3hqR;?6b%H>01I); zkNlU+zDdYn2qUM*vUde2gRE~4Fi*u?GYpGo^|c|-hzas<%u@xVP{V)e%vU}ns9E*T zg}&?@O=802dOqo=2&;@D8@26?V+-4DJyw-76wwpN{?(BklR2I#i2wM10cBRWtaf-t zK;B>fZw?$5Y6VsBe6<=))bw+jcblvz#jjnXIHS-TAq6q14{Fvg0Wx- z=B_i6F+y3-*@6tCo7;9ON1bTnU(*QI|I))YMEPfusPjVQ=Hu8eo08o`zGIO>Y@w@+ z7Nr>enZu4wdz$!Dj-EUP3uE6Nb!-nup2di_S%aaubBR@%Ru!V=UJAZ_{E zX*2AHighgb!R^Z$a)tbIFNoIW)tm2+k7Dk-%#cbby^Jya-pGMqVKTtlPWuB8FY1wj zC-m5!j3(@bL1gbmc?gRbIvh5c-6VK$)5Xn60qt z^)Xy}#q85MbG?*y78k+3eR^Uj~>fck$)Uw9~Pd=&?icfD4B z)|{W+M5%@br0MVUzPBA(fn(3tcgstX$3AF(d&w6jN;$I;pcgzm z1hC5MI1=Cu2$%?$3|%#;!7;(_&dOSgziN+T+OCqu{$Vg1;c3GfotQqFF^ef7UA6!9 zCInupB*5NTrH)2D+N4Q7ic-Ent8Nmvx=jmUnZ@OlDYL(}Gm55*i3XQyWQuzeG9m9p z=co`m9y2%@R8Tb;HkqeNdEFcYfZB^6UB93-n9n0Q%~TKORp3-RfFV_B?uF~9=dBUY z?!0`PnWynEnsL|bl#`Q#r^}X6qlu5;smv`mIOKa60WkDlB$9XWXeqkFd^xGP0u~Sm zJ~QWR0C^HN9!g`c*2SB5$tN`Na|1a;X_DjI8|%$Z`P%A^ZJmkOiW3Kg4}B=(Y3~_7)AjonC!on-6k)4hAjs{4)?dju04nV*|rwv z6x40c*#kP`z#i*II(J`MW%(TCm*_W?d`)VL3{q3Eqc!MNW#nq*02X+Nv)s6mYi> zcF4=cvWI{||iy1frX&+%Riy zCe7k5G`G5f>sz`P3i3{VpD0Y1rpRY}Y*YKtU*FZZ)O`d0`t-tv$U(w0pSb3XlLVNU za_Bl>%=Tl5_egMStIpTpYujc-JO%%8tS`n|eFuo$1n3bcW|=Kg{AQWrT#Of=0Ct5OM1JqX5DWi;pAhn#?D1jfJ^sYy6u?+uJ!Qyx1_q1WP+W3-)MC(e+}FF4Frk-NVf`ceG!zY`jMjs&$IV7D(j3t-ok)2taCb z1P~Y*K5-L(X`2#(McJeq2|3JU?ss=9kxqE}S`V`?Ue2?Vz8~_EX5MP9`PZJQr1(#k zk>16ds`eGtCxQqzCeEO^Z7zY>C3ZE-sS5(I;0paPD}%Xaj_h1Ac0_ z%|=xL+KWUZ_6~Vm)AU;pYQXxoGY0$0M;1VWuQq*vIgFymttk45)k`AcWa9mRx&x4H zmEqlTlbx-L+pJ|Q`p=C_b(_&n$<2fkh+a?Ve)PwO1L#Lw!~6SJ>rr0|KR)1;AWZ2_5HbDj*l~z$ zn!081%zbk3E_>eAhgRd)iJ!=5KaN7}Fps3hN#UZR%FgbvBV2aavg={l7_#!oD>{-D zlaOjrkm@RN9{n94lI-Y=v*oh?Qn;f=FM!9^rJ>Fv8w`t$jU|+I|8)l_4oIEc%(jN$ z)mofI8w&c=;83l30A+L>mx>jCR=4)e9v7J&Sd!DE8EFtuiCHbzSA)j-{o_kwcvar?o4$=Jbp;}}V@!I})c8v5Y(Sy&wAN%L_g+kzMxI$R(QXUj zIglf@eDIg&)9ymp=9ZnnI$$7~=<+&-%moa6LOsfust(zMO9inUp!cUD!$Jf7TAPC* zwI7ZP@a?J2bu~4&Zk)*f#LPjEaa;nvZnON3&od|IHV_0jH->LwaW|NZt8pY&h!5A% zagxCX{C9`4;|T?=TRQCe5pcyoaAaX;z>*k|61CnbI_71KVxJ@NhtNW`s*gd}TT74S zmjyRofU$?iR z^UzI1tEaT*x(*{@LtKL=l=YvH^nMJikbo92@0SGcQ39GoPm18Q za{w{$CCr6>oOjXlHd(?IJ1Iv+JAmh2P<(C?h({O!k#rD9NHj#$*Ta>*S$>5aN&==6QD5%qm!a=;fQjAli+{^ai z{JQG#K4oOp3v~NGkUnE}`fC|pMiXUxH30S$v_o8u4BCIXwKB*IU;34kZ4BTs2p10F zbQzDi`;^#076Bs$@B$az*Tzr=*`z_D?UD<#C5DughCLE*i^tAayxaH;hVUU<#RRlK z0LQ3+fRSQVuaOL9RJikrL`na5ME@yRSU4{H{qrwzv-R70)AiL|K3rK=Q2r29JGP8ZME66x!+rjvnfpVB#H%2(~c|!{Azso5`PvK3_K z_g%Nf#@EP%#0!kfM|B<)5)l%yx^PiftOG8}96gC2n;WXWc$ny$trA-s3&1WZ)F0by z!n1KIUUQV3Z^Api$r5Ntx)L8;p;H_2h=9~D6;k*kmPV=1>&`9)40a5&~ z>p?E(0LL2FTV^4SK=Sxf6C*EbQB zG}%;ugJ?c0FgE68=}8uz6wX#e)oHca6dF8mXBNUahRE1G^is^r2{8A1Cf8staOW)B z+oPx4<`9HQ2)ImPs9<2)x%|916Kv$jSU9w%rq*lGe$~E_;bozzNb$obmnk*NQ%0{) zUcQF>7xI3@pGs{)QU-R@Dpn}e5l+n2=uER?l=$pTkJhH<`crPr-i zGQ4ZjEKdhyrqTe{Yq+X26g{%Ehf%(P`6!x4UY}N^BdIkqj=)?2B1+7;;Q^$KK@W(J z3agOp=~8(zL!rTI(Uddh?7hgDh-;}yv&@X#w2?3Nbsc|un8H&CeF@^e%M8FFGVGlX z%bYuN?RP;;o7@A=udW>fJ%7~Xz=HWx->g89Nl?AOr|3QVw9BYDe677@^Qj?&-z9Kec$8A>`2H)z9Wgt zZd6GmSLF`^{dlWi_eaX|rO(W>bFbEN|Je0S2K})FE$m9WWXo~^u&UDH`=mnyI~`LD zYX;v>kpss-2#A)JmOx}<)Iyi>YRk;z%i~_;o*o;71gI_8x=I}$9!A|OTn7^i0JM&9 zh!Da|HJEv$3LAJloQ8v*n!&rAv5nu1ya+gB3jh81$m0S zUSx8xk@P1vEUrYpYnR(PWUNcmV(igd7AFgh0%N2c7EK~hay4|WVWN&aAOw-6#8DjX z_{sBWrMHYjEBwEZU1>i<@ov@8L@0>j%f-aEwd)WM8H43IO6e0xh>p<{TW(x$4YkAG z&36prmcp28#XqrRuV^{2mX2F;lbZ9(WUg`ay;iU_!r;$F_)WCxdgPB^`0@hf0_}kQ zJ=*y_g^$muIUfM9mfqsFgrARj$s@2^(Oc^j9P+o>>v-t*_rK}HE3df|zce}GO+twA z-8DG401kuiQM>&rw9$pl16stx&^S~hHb|z??V+BVF7;qYkS?rb=b2inC{s2IKm!oV zV&!E$P}hEmAvS|p zeHjHcHerXwul}Y>UxY$ipT6tT31F&59sH(|urR(`D(CqGeaR~ae445ayjnUDJTp?0 zq)#{8>Ae(6L(1ReLR-V%4D)J-JnEQZk@FsZ#1=Sd_p-x+owFJH<*R;`8NXfC*#u*2 z;B!IDj>xyG*^n<1u2QV8v|g&w3Qte2GhH9p{bvO)G6CD{>?my2Nj&cOglRiX{~}u% zuS+t&Haj+I0nhvwkeQGSlOgS!-ieVl(O2#Ur;Se>5|_*!+A2W0!Ezowu2j*HFnPS{b(ybp)qh44{$(@kBg(l@*IOdH=L8UCdRN_Cb|ILXeedh5k^MfHUh)X<=3xYQ^(!t$Dp~w zj}h~z#uHrA<$VN^-g4AMsX*DWEho%8jQ?A^S_ly<;7&6H*b)`%LZ|@CZu#&XZ#QJY zdXWa3ama}ZBH7h_3cc`K_L-W?B?5luxg72reUEuh-xGX8d;L_9#RU7}=>T;Xm2Mp^ z-9t2G+il<{+8Axb8tjX`#`nm%+(^k$QAjYXywXe$CN(&*v{V(Qjl{P7svWhZ^rvjaY?cSozW0a6 zJg}r=m9V&@ktrHW!^6k*{HSBv4}%&gDcn_=bqangC|O+&Bh4jmbZ&Q}!H!u1junI; zNRu*b_7>Tv(;rD@Fj)wZR12sj#6j#83zqh-+ zQ$LX1K5oVabc|n_^WFU5;H1&cls6M!R;e zkrnb$$4`B+vJIqnGO3{Rw+hz^@|OXm`^7{70{KUPxqoXfUW%b6zLO8lBo7rpzun*q zIG&VbF*X=cgJtxMV=*XJtqF!u=_Af6YA>pw^cl)>kJUxG{HwFdIgc3acS%W{3`eW6 zw$IlO%l5|)8(Y>&+U`QXapr#D)TJr@X$LOxe)e*VVSgOarAjal#1HpL06G{-I4ifOh39;! zcy*G8>l#WwcB#1wWE;%e$toMrz=X2|E zpTGLJ4C5J9G#bLboS;H&!E z;^8D(QEV{Hlo5fH_h3Vv4&Ruy6EhL4BO0wiUakxs?5yap0?=57EGVo)UjSM@`Ih`sFqUO=5i`TtJ%Q09d-_UpANkCE_MZ zZvm^TS)dom+#Qgk5INY(Y#V8j|3DivrvKcR6iHjYh#feWcAcqWt!PSY{lP0JE$k(< z`~dhCSgx2#Oc`*Rq?#t16Qdc^kGXA=Z6m|Y&3A z2L2@WF8R$2-)s~P^%ThN7kE$__9TKV&>sclo#wBVW%w z9Cp;_QALQE-{5K=&QbHWh_W4tJWTlGV0>7)Nbd%XTM7VIk?oJ-C`5p(o-Jgzt*}hK zX6VRSEAT@CF}wjbcKv^To2F%CbQ59yq+uun)VBrpxv8y+Mm_=B>epQtFE{Qr5hK<< zLPROkf`hvwZi#Iu5yDBaOlm1u!*G@7UtT~aoZdPb$<2%Z`LC~ubgoGM)gSAb)cHA{yHS_>c|qV zzK6O-y}4T8U$DvSkfr)=(fj6eWgWitTho0~&fv$Xc;<36n@5*~!&vQOtK(-R1O|5~ zmn9{-bFofLoa^ATWu+~_jyhqS`LZv!pF7B!webeu8+Nm*SfbZpoef6B;177pf5Uk= zm>C26hsJkdc4ATX&KPdpW_(9WvwRoh@`?at5~MHljAa8WZ2YTRZuMmh)CsbrkWv>eYHzzvA)sZjI00aLeq>J;i3VjoT@fm}2V(LtEku6c&!u zl%gE0zi@;Au`msjoLm(aOqh4B8Kn9@`1AJxfcBz-!}CdBA+>dID(`9>9Ybth-(}=i~_HA z06HT$?f_f0l)MVts9;#7#wVB8MEC!SbI3#?*%)HOR2smuV{E;r)^n*EtT8*C{66)u zZ?U-Mx6upm0Qz&2l_=y$DAE0&CyVM4loQLku}9yIH|za&UV_;;&^Nqdbv5hPy?Pes z!4fel-wR#D2u2N$8vgjA&8{p?O9f_XdXI}XjgIDj=<3-dJ=O{T$FdSEsV!sb$J*QV zhg3-ZlqFNdkutC4?-!hExHiOS+K;m5>^w8)>9Fr9o6;Xrx0zKsuF_W`>zN_ulXC|5Zyn5lvx5RrjSo1_ZEtmVL4k;TB+; z_W&@!uA~DsXVj8MKe|JE?MTv--GqZ|d!TxTXU`M##egb@bR=36ELOwMpKTmdCZ=Y+ zU8uWiAgLstJ#D5om)+BOTp+Yb-eiRr_c5S$7lzA7ttuH_(^Fuqtg@qlC2!X0%KtLo zc>FV?OVA~w$-Qa5MbaQqUI4jCP(d|8xE{6ZvxP9KW5S_C_-i{w69+rTAa>@EW*VdX zcWmep1=(BPiF`>=*y!kiDE1kAsk0q19^|={?`Z%2<)F0a2|F111cg-#--I*%rH@D6~rz3AEXuGV_|ie>QvhqWp*VKelo@qrk@j z(|o2^HUs5Y#%s_uHyjB`c3UH>Cw4zlRMnG}Md|qpIZMEy1i(xRzLX+V0}NWum-WFr z?!NuhG49yOuGJ|BW2Dp;bDYR6A!)VPNx^(nL(+Z4kJNm-=++Fj8VlQo5}Em3GF5sc z5EdBUp_3&5S%Ljb34otu-4_`W_sga2@e66G2;a}NYwU#j{Kh`Q$#PbquHdaQ@|8~4pl1l&(NIDz+0ihY{=S9(_ZM1uxo^e86lA#yZ#cUA z1{-tng(H6|?)i^4OZfTS2Uk+yX&4e$_h=t>#~2J`i4}uc`RL z76yA|YU<$xy4kqHql>+)JX5*M>dv zdUqq%-4Q*i@{n=<>&)(VyZrpQ(xFL$W=7vLeJM9>&z|cbFY<_|Ir5j5V3L2IG`30) zc1H#6%$!lT+iV!e^AMhIl|Or!@ApOqW6u$|y}Q7q;ryAxvfT7OL?Oy2m|leou_(~& zJXDyBu3a;TGPSH$Y^LtNP*lB{mL58t3_95`YAC9rEK#Bl^x6=QXL^#A2|9VGj>dew z4Ff163;Vg+a&t9-_AbI6=IJRMSD)u2{xVk-Cs;c0(+e@5^EakH1e`vqz{H zP)O+#@`paDTsyY$3C~?*BSiYDpW~+bdG0u4?l$8{6vr^Gtpwg#TD7F`A;QfxDBa&P zRM8NEHYS4Jq2qf_YoUe^kUv!07Vu$B4mS1gPjWD*J&n z%(L(K*|HlYuY1SlLGS4ysZ=Cv3t(6XQhTwc$-@tH4@{PX`ei`pK%wRFma4+cUGswg zWgOr|m>7sTF6DgY98RT?)QCH!Pdg0)Url9F07S8nb@ne-uf9gGSo*Q`eq>uYD=@ej zs>$)1uhq#CDJEzhGJG?_R9YrZp%zQxee$Q%*hgQYoK&Dy(VpqHQxfeW&P)BzcQHY>uZU-1@VhREze7VmG>< z*VTFQpU9cPua{=;S2)I)CBf-soE=#Ih=qQxfBfWdhGifNO>p0S^f)f5&epy^dxD21 zp{6(Ur3{WOj?w8>^ABx{)nCyIzTz9bx3*vT`sr}>-k0wSQM5XWA>^;rh`aoDW278= zh}`dmx{tiX%fZ7lK_2ntWC@%*0H4MjND-U*QD-V=Ht&NRftvk!nfst;rW@PqCwslMa35>lJgiXOMr z(zCK{uzp0$W3J`1BRwukLb}PPw86WiqyFdW7MzY{UV|4KbPDZ6gozJcR#0J_l&axy zRtn*-#mJqnm+kL%xc;4M+*YdQ_g?$}ip(<(jnU+sAhRH((-@1z#B4l2{Cat(=IkcX z;bxys&6vnGpW?@QR7LDpP&ay;VUdy2{aJ?2ofCt+tQ$m7`t{@3KK>Vuo0s>WZ+gU* zr}Afb(e0t*QuoAJ;0Ei~?Jvz|5C!=Ym0Vd{**sh+GS!%f{vQR7>gbYsGSap!@%gIj5l*#04shYHbgr z@)Kumog6as&;f+t{NRHVm2}IgDg>>nA_X8ti6?a%@(W1YL;Kt3ZeOo#HgCDy`rk5# zFqTO;jKPz?-ci_qsJr+b4DHWSNeke%prqQ>7*k6J&IS{cgpBRiA{jnUY%vROCP_x_ zF}f(>+YOMiO3SZsTHmfV!sVq@<5{vVaUZG3szgJRw3y;Y6PX97VLMkYq%G@1S{;db z{L9+9T)VP`e+Osm^kR#wBiX}+e>8s5>;oJF(LNCFy|2}wMNo zm-x=%^&NY{q+aQ#TATCere$=(ogMHQBBfJ~94T1fLmBI<;n1W<6=#7o?&;0YV#O_s zBhK#t8ExHSt&YV@3(Ft^!O;JkDnTIlTK1LL&Pf%0^xnxg z5d7CR*e;ve(FB%Uc+blKABDZnvnMU#&us*=@lO6b$(T$g_-qf@@oKB0n{_t?q+!3y zx0@`w!&};a#&3MDc+nFh#>JX3wtjxy5&4pjb@t-5;!}TrXIdQWruG9$4Q*ZBUIBC2 z)gLSz3VG~rXtO+muJ4{OKC^4^_P|9a-p;AEyRPDC((8|fD7aAF4bftxM~~xB6Ej}^ z5U3BH!(PD)Yjxo;7Jh0mto!wY*2R=l=?LXOekeG*?xpdxkC~caPt=~4>XZ=R_ePJZ zJJ6oHt)*WoF;BXSAPoO-HRLB5iQ!mpbIg-$mp(HVpO*-RPo6u!wMu63qK{R}wQ&Ba zsFgi&-AvRH*y+9u-CKp9SXQb=QLBi5-0kyEudly*bh_YE(NrsD(5_w{{WgN}{lgkS zAR9wXv2B`$sSQ#PL%)HbDa}}^@`xFQ;Q4ww*a(CNC_EY)%N8R9ri?xUr;@wi4g)|6 zUA>$ScyUfTynq9RUUgaU7@j%Xupn_`SkBnxd}7=r$TqG6P4A<8p<^!Du_ zz8?j$sM~kW@ZT@W=-I1D1s@#xSq^to)B}|Hvf!X~R&f!np3-oBP$shi$=+BVY+`~( z{^@*3z)z^rJe9IPXTq0e++VfCIFY&Wq0JzT1m3g~9Q*mu?^wTTS27#_0%K-GFjG{T zIRPWv{Pm%n!XIBECyJ1uTS+0psXcr%RUn1S!T6S`uJ4Ycerl&4-#ben+n?> z{Kf4DMDhVAy!}KXYlS?L!uH9$F}yWck|Ca;SPk{F{hZeJ%hscmPu!jt4HQHVchLK} zXB{qDRki(uQ$}-Nd^d$^oKJ7^4RukX@@SBqR?ujJZG_o|0~uC3ifRKKcwZ&iD8F$b zr;idh5|$6{BK&1c<(OCv=iX(BDB3A|_HoCqne3fP$+NcuUMCC^i@zua2FqX47hY^c zRgCu`8~e1M&Z9L*yTa?N=vlnS>D3Gqm$gjkC~%HDobV4U2|_Ht5_G?B!+9hIe%=_~ zn)wTN`nhKOr6dPs!)T;`oh7(orva?|7|%K$Znr~Si}&0fvtA(wSdk$zgz!knQ)HdE zRmcZeENCh~1PoImYp7SEJ!AyK6lMqNv`0?tL7E+2fE->Wur9WSh%?76R72wnTb%(1 z2;2+t!e?l&FQFBn7x{X->7gyBuilJ*tBHL_oot(tOZzYD)8I#H>k*lH(XbpLi$UDk zl`|xbtnn<@carCS4=G7~%WH^fwV1*Q6SGG`PKHpoPd14nw(%t7{T+E<{_KV<WrUqF*a<~J)9P(L#}n7|9}oy6&8viCCO6g0TV)aGr$#6ps>!*I%o^H zT0k2OGuA{8kp)wFGWyf@!8o=Ms%`8&*O`6P($Ww!jsU?sAo+TZbC!$x(e$QJm9 zmkLrvyQXQjtWGC?yCG$a$vj^s16IF?f=4XP8<}13fGooPM6{yrotG+T>`|PjrF=Ho zqwieBcZkjB{5i6rSMN58By!FcjuM39)cY6lmBqE~iME=b68HYnQ)5?C4;tvUfHs@- zrZ1(8kowoNmHKs~r@W#eNI&OGH8aG`EBO?ic0Ar;r>Y+`a~C6Q_dHKgzuuu^@08!t z8lHGf#s^2m)M{c80PAHv^gn>of2JMGtm7x306DL1)|Jaqq3hePdU0Uj0Q{32AX4~$ z(g81!s|0xRnIFdR;v{>B7w(-LMqGlefYaY=^6?T9@0wVwrd|^~AZxLFht))+LokMA zEeqvjTg>=pL@bR9E2;(&8zg}xmL`%(aY)^7e^!bt#*RD8OS`E6Ytm>Hm1k!mmt$(R zc5?Ry>DjM`|5)09@;={e#B2%st~^uu5!GdPYdAgJ--6r!@5x=Y3SXUqzo7cCxu2_^ z0II>6QY}4Pq_r52JK@2Hjj{o}DCJ6?M@lgGdc+`)vn{@>+|&mK8m+yd+ds_=!mpoU zJ7m(@I`~5#y+1XTv3bUkTKRXse*_b9SF@G*l=28Uxmn_MF>D=;$>H6_&N-2Y7_qyW z`ji)1H|0wT(UzsVTLwjDW*h?pgh8#>+jo)6Dq z;u3P95ktHpG1aabj6Xd(xY7*hPJ3u;7&lD9tsd5P(s z6tu&7{lxS%Nh=Sgqa0o?a8pseE+iMs2LYlA<5~spVlWx-Pe8w1#*HRbYPiXO1 zJa_uT%;Y$Qc3_|}`9z*Xwq=2k+;^M={J1dX3=kwJaJ5jEpV=VcV;SdmaQKa@{6GuI z=)VoWCt(wQA2Ot#*m}9T9Kioxj{9{L5#t+X(@_ZgC{Kr+g<*?HLuEVM`Hml6_wozw z)eZ?#a@ot-dg2x2$$`vcr0Pt&2x=&Ur+cIb4-j$FpBg8mP#9U0P7!{-l<_L?*0P>l zJ1uO;q^bD5764KqQlVxo=S-lLG=E8Kg>Sj09TuujHnm&cnq=HycKRhY~o+F09EkSbkxE)#`s29IKv4VuoNvT91zt!22Akep7-mIylR5 z2nlKZ3absEyKxy0FfhBG<-ScOg^2o*X{X&g81Mt4WKPis4gQhPU=PF4u%?S@sUhw+ z7xLW9tW+|w=R<0zb7G0yG-SQhNwtG_bYyG0TK`Ed2fC z`MX?A#O>>2rR1H%DDI`x=7?rL*A13d#Ay2E?KRcaQJtcoAY#P|H5^Q-lKaKMks9$^JJ}@2fMN^*vXgq2)g&G5 zM3|9r3_HY4R$mSR834%x`tp7w)9rs!-u%NUIDbJ>48R09CdmMPx&O|cvE_EJt)$@- z*&#we?h^7dOoRYn_H&j%U7Yfi(e!6Bu)D ztnScCrb`@%O{$pa>Qc#z0{xHJ$Y%jsC^`U-zJE(ndTVdmbACD zWf$!7<=hO_C4uuodCD(SfAFzR19P{GiD*`WkEd zg`%n@wC@-<*+&%g2m}5d8lFF)yIG)?`2WW%KMt zm7jKX0i^hZT8~&U-~XLh@Z}zL@t1tdX*5iXj)#s+S;It`OrB$LXcktBT4@SgaT^~w zLz@gherCxWGu!RJ7^ituG;Im3&(dcQn$kU$cEsjP?UCcNk|7U->ZJdo7RU&n4V0gd z+z>wzpai`KxEbF^N0AN%w5BoGv5?Ij&!pV^~`6gBSL6C47YhC=C3hfKV z0Y}jfBAOB_?Al$H-0cGK{~3zD6Rx0+S?1GI^&34Pi$QX+jCu%{;RjU*2GV0 zVjPk89oT`QNeZ&}yvM6cMB#A*S%mZ)V-Ebk7S=XtEQP`JkTJW%hXcv4mC~7ne@X$= z5Pf1LQH&p<6ip;=s;%Bp@u<@qWc-orFLU5g+1_SL>h6x4%Iq?fwXMAS_8v3Lbb0L6k@l5SWfg|$MKwVP=dP`lRBf3*0W`2i20c;thL4hah`SuzN! zM+M0q*=w`&ff7S>r$dwzK7M?yoIN4Nd6}7(FBGivWpprKi_~|+Xis4Kg|xf>XPoO3 z+bVGp{3<6^_kWeG^lpR1L-9kU*2;~oWlRc-^Bg0ei2L#z(&QRKuhfu%yHZ3|weXQU zu9a1O-8Vkw;1h$ao+x!>yH&px2j%mhj~}19Ja+e#s9y@O3ZXnsYKl=Z;1iJ+;0Yb{ zjmq4pj1~PU`DLB&uWzrV#ekzu(@&1*TC{}`bOnrwxlTEOeU@)FG)DyI@mU2bZ)!JRi${+@&>XCWtN&xEMn@?T%%y z{cos=U&ZWEDn8}pgkT7log}Q5Du&lXbU;Jb%?kmh_J7|He$T6l?!ewStb}2UE(Oj6 zyng|^R1ytLikg~O@2<8Kk>QGyVz6#CQXHsvbJun% zhN|TL&TCxnuvAPck&84ed^v(kOu9$v=yO5UKR^mbWcU(CofJ4=&`_9cdBbs}+ffk} zlPCuq0Yno(#aw>9hDr#*KH*L=@YT)$4@b(d3km8? ztkBhQ<0OARQN}lK`_wRv{hXx#p9h@;)AU?CgBF=$myEN|R*c|o4eoly+C*67VPJ~L zSbXbNjaZV>YpG}kpZP5V$TpPA3NO3AAih#5Jn;vCJF4TgjGJC!BiqtWQc_Jy~ z?A#@pzn-R*CcD;lPS1qV$ZjKwu$=wn*k-w-#ZbwMM;2KF0eAML1(UE3LHXZ_O+>yF z-~f?LNCG^&&)r2$pbP*%AHj`dQ*8m5NYL>R7qr}U@FgGNlpgtmxss{|>$}fiPbA(^ z_+6C)oPaSWC#NIxyPrB*jQ^>1>d*Z%-7tb(Qt6?=S8(W8F6YzYMldV~eI=f6VZeuF z`jE!^3t_0AEbJ(i9n>68JlwXPaY&GxUV>#HZh44hcaX1n|07f>_KIQV`dS)*tGm5u zJEAoR3e$CKKVQM-;T0T|WANRqd!n|CMFB7_u+L7%vm~;_`C0GjJm?{AmDtEkRkOM%B#_l#i|+2{m!G4L^FzNirr%AYDth!_=x@N)=G`C(q`F30 zs&tsTJ$H^co*_(;le}Y!WX7=DePzCSvRP-<^j zjMDgV*UW>g8>BI7_L3U8kMpvN~K>&yQ$#3lTd$8uRQ%<)0}> z%QLjwZqdlvg>ML97K01AMWxVdd^rb|VhrRd(jALr`W=cBk-yE;%67EgbnoDef9&7* zl1)hHGp^MI1FSI0Zt{B$Rjj)^`T6dTls|$b5sk^LXcW?ZbV*{I75P^U-P@ zVj-lfxnN7%5`X|3=)G$(VlH$J{iD0MHmhRe*M@ZRmWY|Sjq*%0UVs(958n%PZ2ax* zfdnxz@z6_2bL@l&0)Pt(M(A_0dJlS>y^zAqTKU{5jT`nI{Pp2!()UC-cUN$MCYZtt zcr?ilTLsa&Vb$&9xc;EBZUPziGIFs8#ui*7-!=S@qEjOI;%2oDCwZkZGAU$Y(2aJ zJA|{kK46GU%KyUb%r`&SF`zwN|W5s(z zXz7)TAUdb*#C|g*O#gG*OQF$N%bwuXHIG-sda@R<7_^qdtmsp?Px6gh)j6R7D_)qpT67*K3**nair+<4cv`(?MZ$9|325Cc zB>C`PSh^^#6j-c~49K6yVe&r7T}A0 zMfxLoJ@kwS2l9NYfPnPnN0BMrAN-6!_O!2mJWqg2 zSJaJ$P)=)Tw90~+uf?zgu!c4g6REZPqHpwwcu)&7T*kV(5O&QFH1Afs!p=MFKRLpM z(JJ&9$H43w?!x(YZo=QfZ?zB~EzRB_?#PX~@dRP3 z*HxjB#T0%w!{j%NS|rug8Q5yi(31__B68RsZgnuerB6kURsibwW1c3m{v)=c%8C5* z!={8UpG$Eb;x9Ihe~H^v_yyv5H3iKqjA4RFK|k&b>4v0pz`8;@spxP&Wz*M^a1e(6 zG@|NrTtzrLxz}oJgHE|Y(=w(Nmd0(azYxwYE>EpNiP-o+V8ahnul=baBT{1GAaNpK zlPGi>fBmH3e%E8;hum+`c5DQMK$4~nC)Hh%8BZTVQcq}QGD6G~7d8TZxcR;DNuoBj zrV$V!j&YN(DHxjeU#3!~-Py?eGQ1a9?9Qrep`2DuaEBH`0RdQX`5Sf#a z>RzkXOSt3GvejE>9F*I&e0zb{+nSDEO0-CFg;OBau~|3c2tBaY?7|XQWr{-?D)27k zIWjStGWidHOCpN5q+3Co7bGGGG=qr8d3c~bgZfrfw=ckWYB%qR0qfJ|3KgnCjZo-S zOEQrE#hShY9diFw82Ce9iJ@{S01cDfZgbTJmI8cyIZu9zQ;XB^1BKk{5g3-ErK5_a zdOr&Uu>NI&eCR0yOcJh>hbnZSUsKi38q@O}?LbOJ*t8cx4N`c6;OIiu3nIJ|DQcot zA_z$9#8+VQAL&mES$kghmywF8HCVadCpCNHG+h0*GKh=oP%=J@;UyS5lu{v-sUU9h zb07CQCBr<_&7VH7@jGd-ZE3{cmsKpeTxyZ)mzrF`(=%mk?=ppY=^x{sEPtnc|A&FT z$Kbs`8YR6~{CAN@pXnql#NrDLMje2_&SdXTm}DW{JOa3J2WUZ>4YCw0_R>rx8h3a1 z52%kKH;CW3R;6`1yXI1_R9LW5#^c*pzbE=C=7%P^1rfAmQ?nWN{CHp7{SUzNWi(_` zD`DMRy2!)%hC^&mXeIiJ27m3N8(bqMD00svu6hx_FU_xxc3*)=B1oKttM#l z2JF1cLu)F-y>#NSWGodF3q4K+=}&p8omSz%;{X_`NIGKRC4v@bCIY= zv!i(1y4V!W4cMj8H)nYV#S&X#o_m}O5jiKgQQtSvFBp0mSbyRW5p6m{+qzj$_9pDL zu9Z_SlOz7AIHo;Oof#>Ur4Oy`tdHEvLd=wVaGIzX1 zrPn-97CA0NK1{rPAsS=L=x*^<7irj)bt3x~ugyCqEGX)biFfpkjArYNrvZ7b;~IK@ z+Wls6n4`4XrR?V93EglXp53SCl4V?G%kykCD5N#F9q*rTJI;JsQqqYL9!14du^BE_ z%Nb@X)`d)LIeY;6=W_V=hrqDMjJMr`hG=B79Btvl_#h5!JMUQ7aEA5c4s?Q0=w1N^ zNyNbNoCx*=Xpjze_>saXDUQiGOZ~o}dQ2kJ$Axgl;7$>Tu28~j zwoq&J_XH%@iLrif2=GK3M3eOiXXr>-F@K|vl(0KFUvjVmgy5x6%qn{zKb!OO-{y;y z!`NB9`|`p33CD1%;~de;>oC3t>m6k)&hL^vVqyIkAm(%Q`@v^*rkzSu^k1u7 zfZAA5dUL1e_;70seH6#c)Y%#K$xavaf>t`iXo7A}<1@~!uV%b(TAe0-6RF7DrseHx zW|YhKKYE-cwx?k`FN8(i`XsN8!z?||x@(m<@yLgK1_#$`Mn4sP7{Ba9kid;_xOU!= z@JwjE@7Bf^6}28x%>l5nbaDZIP^pIY5-!Bwft+agH0pDY<{XI`Ce-+R`Yi70hGf!_st5GJa)h-jz(s)Xo|N~&oWqWunkQzp=)#J_owf$& z(KX8-l+u{mYv?oL*ONDnDe6|wdw1NL>tEWh6YteJc29m-IFjydw@udt5h2~94we{j z5gS*{^H90jt+mxT+^7-%mVyiIQ=)bSdz&Lm14I10Y}%uBfM@fM{|uRso`AVtGtS(? zHns}CezVjyZ4hB>8Sg1pc^`V@VkGOV+e<{x+TFt)M!BVTT6K@Rk^Sktu>YAs)VB-u zVz)fHFs)j%LAoFs)KT_tPX($tD%+=YTf#5l6U9$hu_8GL_@CkWP^ zb$8l`sYa2(Sb1qmLCHxGL;!O2RT8t@U2%RshKXY%bQy(xe%;RKMu4{oobF#?otCl4 z){6eF<6OiuqA8Oy!#|+B9MZ~!2xZz!* zTjB}HoIx^mbNdW8ww`gVl*1G(k^J$9W?;I^(!#xc?U$i+zDs%~eLz8b3;<-_Tk6>(AkLZTg*eYhM%m~P4 zVm`xQPj9p7&_bz!O&|6JNFHY)w`}4(jZ}ao&*hrB52FQ>ZlbDeJbZOm;f83-&P9|y zrhn=M6BE;Q!{kJ+`+EWgLK@)4D%Dg@RyOcqDp-D$>+Fyfz>AxjxfH9y(d?NTrVG4U zJBgauDw$z`RrQFf7}F4X&1BKSh?OeMvvPWu0L{0V4|`#PN@Cn#GHqhdh^LAV?&4d@ z;zQIa)@L+cy~p+Ikds^uvi?osWZi$Ez2ad**dsfL!n3N zUxkBNk)VFV0CM1ysFT(8vYZqFy3s=cnSv(wmqwCgBO$76j2MTJ@kKbd7K|JO%aAte=XAVf~ zxN5Kk7OOHfjE<*p;-r!XY65b4tw-H)lCYqtY@V?1lB{l0HwWrn<1OPqpRx+K!dV?{ zsNAbs*8dprS*L;KKg(gU>1u%h6qBPjQ5_RQXt%{zrA3g1m@w}q$er);`rU{(U4CvT z;+iEgBtQFueh`+75t}VJk@&Mu79x}iB!21dn?~ z3L11ghx5~~08_%vbFe2h9bFcO3I&i1n#wQ?5fq4~*zlqGjMeIIAQ4N^E{H|6_Kg_=Ro~#;) zvff0mk?rHcczg?;o)0izwEfOIjX06#xO`VjSota{?hkZEkX0qv%YvdgOazpe-aD+I zK^~ZBO~!uH&*xSJV?JKEj<&3Zu-((n0a0E*YI419mM@u-9h`>)lHMvx3biZpLJ2O9 zt6ux9YAdXKbCv#?M$_~6eNx}GX0kW@-c`?63<@WGHdOaTa)UO zq#p{!6W#28aI-3Do{=Sg3{pbxv30ZG-yy*$9fg^NKI;1 ztjTL&ulroaZ`Z1x20tqW{`RwEiqj<*|~$84Q$%s;N= z7mE6hdF7>FwrURs`<|=|2mV=qxH!8AS2=rct(^Rh0NL*zuHbKU&mJ{(>BuTGMe34N z0P}6DhdJOprgU}h_O$^lEcCd&@7CrQ2z``G#7n(7Vce%uO^-K57jx16dhGFHF&pCn zB#Py27F_7$E-ee7t|s*XR0L#_J;2@frW4AV0z6|AZs>2PLW`hT0?;v28~ik&AH&~5 zHjQx-U~EW9tekoAzGX{9g$*hq2%m@(wC}=O32q<(9~v8B@TE1F#phAqH{2_J`%J>b@emaistG9Hk zYufmGZEf%pW#!Wd`WCFxO8Ux2G=Y(z0mo{W!tBnOMQywIDk%jiZ@Ob^w)Z;9sfRGv z2h0L{!-&YrGZt}1{?DIZutpMS^^Kivc?#&^qf(x;P}|%03XGVB9-TPC`gbMF&AH3i zhvH`Uyzp5B`956D5G>BWAv&hZx2k^8dhx`LXKsN!1w2Pc7GBVpj+lhYf_g4>L;))F z3w|oLFZTJc2Bc$?6IO#kL(-SL9-P8_0(j^y^@2QU-Pc1#o{dcfOCui8*r$?cOkV|P z-v|>~^|0SWhep(*N7EbNxZkiA4l(G$ca)Tr=)xM1wS8}3!B7XRHW_GFY^V>R>q3S_ z6~*~B_9(I9Jb?J$)rPf!CT{)P8~VyX$0TF;&~ z`*2oV&}!u0C%z9XzWO9Msr~z8d!Jla3sq1kM2wT+^!YY+e{F1hjtuT1lXsFAO7peAd9@-Tj4AhNDA;Dn;D}TWBePM zB-z&6RLv{0w$ge7)sjTY_+}dz*eg{0Y_v@1=%To1lbb;e_ z%)ISBcJeWl{tWk*89orb->g5&LK&d{szDrw;P=|YF9l<_k^|SH;VhI}Xlu1ss48kH z+|06f)TbFnop?*H{uE{vqg6Cj0-@ zuwChzO21lG-}POTo)%c@F0j^Ztf32rzOB>llv!mIgUFQMB+xU=YbEwZExlUrU!)53 zSp)f%D_!xuZR$UAGvHCF27T%gQdWaPKpd`*?nbAqV)4YQX0+yeQY_Q1at1`#h=>-0 zWR$Kk8yB8Jxr~Bofxo*qhV}%EV z%$Sji(n3T^%5MV0^j{t4@-x)8(spW)=XmwK7jIZOOd4Q3lqIo5|6rd|cpQeUY#%4Q z!Nk*B*Yl^HpSm2%YR`KDOK?{puS`v02Bvj}|AAo1IHd@!dF*5Wu%p}FGiRudLTrdJ z@fDSQW9*s>U<}5S4f-_#f>D%fbQgHt&Zp=WNiHwC_6<&SP!SM20j*zOe#ZE0ivqlY z^2J#bs`HD|NjKiBuJ5d~?&fvA_nRv_q{4!U&V7DL$5`R8UN%A0@vR7S=a&mFVo%0o zELr(La6>i)5Ud6ZIzRmZ=Gt=Q7#;*lUL?+m4|clrwL`*WwSq%_Attjv*2zBv;b~{l zFh}2w==yCXWcGZzdTfo<(77nE-gSEE^%d=OkAAMK-1j|G$g(S>C=Lz8;R1_dK8A-& zt$zm_8yk}vv7K<{2;xO?{{4}VeUOL+ocP%`V_%l6p~#Oz{=rz)p6_!y5hnLQmDcvG zpc_4GQp5UQpk8QJuNCX89>~t#n!abR{24aX6z~rDh*z2tu1WsjFm@?e`SjPkn(?eL z?lmbfJa4wnGn+nxZ#I#hyd##pCytjlLS3wmAQ6nq&A{aS-O4j~rZKfn1xH?}C zif7kOi0|)L8IwlT&q-Jjm!o2n8Yj(lO|XeOlt{jNtp2WS=`d3nyMOCS72Lb0E)E4% zp|xK22B_;cL_8NsdM9!p;!2^LF0uKYF`@ktk@5arDcEW%2@j9rQIAOy!q$*=;WlEp zKozYpc`}q{iB+Fj*Y&#R@>V1TFf)xrKOy3`@auWiel{=}wV7gLav$|!v~rpUA_-g~ zP`^YfFnz2TmGLuzBEWKLKIB0h4zOq%t?}7^)%lX2lxN z{a$|SenQAZb8c`%JF|sh>?N{_!s{a*0Om7FlT^;{C^-O`{iIb?EXJnAW7|Gf_sSh# zGav%$oSi-x5GQDlMDJ;Al{8Q@#Ct!2Gc!e;v_5W-_T5;P6|Sn(=irq7)HvB7N6Wf{ z&`2yWxCHDUlD6E%u`<)wTNMwiL6>1M82|p*aCMj6zL?7Q`r3GD?cX7y+Il*Wkn^Td ztD{)MANSJA4-IB;0mX5JXW;d}u?b92xj(J6cj5l+R5L}h3aY+{R<{b7I$cn+x2 zG4YfEAiPKPoKA7Qft56snoupyQy5SN2s&h(Y z53Tn33=TGQskZJI2D#S-G&1{U%AZ(QBEN^~?C-F`qg(Ppz1@Otkz1PMcNeSor<(Sy z4b6De55v-f(>d1PpP?Q*pgtZ?#waL*p!5e37}0huTdP)$gaBXsbwykaB}5 zQe3>%yT3vfEvj^=@Z-sB{9kfrm`1vS%L0L@p9`3X=v4^j))PPzc9XI6)aunHA6+dR*V|qWe|X_s%2i4 zN5C2HUoVT9oTq@0RP-`#>5W!3=og?V^b%(FA&V}}g{~j^FrIq-+v`zJg}^{8Nk|5q z`#WNtJioSI5bbS_U26k<8~7HCa$B%y&-6mP4eA_WOv{V+oO6y_AcV}Q^+H)QYd-+$ zt%Ve86F(uE*Vla#TdAI-cO*_$RxNJc1gm1QoPu3aV)!S{uOsDsx=&Vo@znl4u-g|e zyR@kiI@o;kMgV1un1tS2hTrLO6doO*x6d}IAOJ#_wQ6UNn=p?Yu%sg7%hT+ zt2TJQP6dD>L~3>E_(OVwu(u&conNY}|BU}#s7G@m4nOdt$i--y#H5Hw=( zN+S$lt<{becQ4XNj&o&%&~XnpV4vqaEr5$cnSiSA=DDX__?vs1cDyc zx@4&kT_-H40rWjK6dU_1&u*$ZD-!*Q2|BW)sZP%r?(vpC?AwU<4T&UViL0Zj8nU{m zkHwxUcDi2l@@9KjimJ6u>05`UD#wGLY|Ckc#{6cQXA)^^Yv<>a1lU*qKFmo74q!*m zn?^2V5aRbSGel`liuh@%Vk3XVmT4R4LM)|4h}IZ#B6Gg->X{NoA)?lqI=SK1)#+?w{TT8&b4s%m=n0u?CSE?^yXhqfCynHtzxha1coA6?8oRS(*3!53*s41@kWQ zx~{kEX~J_cL&yr@l_h=Gf={HA?#K;X;OcuY2IGB~|9@K^%=uRe@-K?%4<8DvhtD&~ z_;k1i1|qlO7e`Zm;wK;T8i8O5x1bIQ!wi9ZO68K%uz>t}|-KZM>Cq2|F=z-rd(O)WPZKwb5J2 zDX5c^73&4EmiCA{HMDY~TOSlpPk%pzzgozMj@P_TuLgV(_EtwJm^ZtIVEEmVx=**N z^m7F}EV66-X@4{xn6j*_>`Otw@mJqQQcb$Z4X&K+Sq04F=9yu!Fgx3PHC3d{VoMnm zu__~6ZASOm@=SPIZqOEtF@3twFt7R3q0YZ7f92rShHi3J!1hqM3gOGwSQsVM&@dW9NT)oz99X7QeF59#rrU zR3TDT`6VQdx;Mh0{gw@DjnBqxL{I|)Vc0T@eLU8rMei`xuJu{DxyUslKs^SbhZVKn z4hYc?&fBy*`UL|g#-nW^f#}D6+XpYL#Y;W`R9yTBywYjPxYMzl@a(#%eY6H{$+{?!wka&+OJ0}mC5PNMtf#6k_;o%2cO3KE)! z&fG(M=pHi7yt)cV9T*s3`PK4zHgoX_qlM;*g1Z3!u%ciOCgk24Mar@}2q#1W>sl1L zx*Z zcVvV>>iaXUnp!L1t%?65s_c%kike=u{TFv5m}!QP#nZM^?}i*9`s-(JSTi&2hxa8{ zefS6VQm(m##eL~cU*+K>Er&*NacyX;;n2(VNO?@&NR=kBL?zXlU~7oz`+WlSE&CjW zejIuA+IBhsp(9m~1K{{TcI)J)+B+Mtq=wJZACl<>HV#KM;PdEMX59Kr-w6exWY@YI zQ^?V`4aKb%?e{Z=>JCVqW`mY@(QDNv_m>?ZOu*uMB-R${OP$%*e+L6(w|gvcpg-Lbg*Ey`cif%oh~ljMc1Zlnk(UkgAW-JZ#SRBJFP8MDkt}PjvF**7o{_5_<;X zZ!Td}O|PS)6;KS@AJWbt7O&qfXPYNKR^(6SAT)e0kjnvaZIYa$Ia4haVYkG%KFZzCfcs+Nr8kOdWX=F4uXIP zN$B7$y@*mpPyy*mhlGwG%>pXYL_tuR(p%^#ND~DCX+aWt?*!gF>-+i5pJc6+HEZU~ zb)B=%K6|g;kJA#%cvHJXgGA;ZMKV&~pJTO)x=h;Wzw=A*F+V5SDfE|kz3)%sjIB)PKMf&`KkfPkqNUWmum6 zJIkH{jP_mq14#`)P&@6X7^s%~1r}szjpkZz78bH6+FZdZFVUw<4|jO}td2*zF*Zs) zoM3r$;?Qpi;`{tKzvOau2Osz`lg}sVnCrZ&V&mqjQ%#ox8&WvC49P(;figMmzKiG)~ICOuP<_bZxwO{ z3|ls>>(bi1cQiuiTx)*cNW)D!v2?~>MLDy(6c8*`ovQcVfe@AU*-&uG7VARWz?MjI zn=iSbKkee_^;e{*PHSRSuUnG8)AHAWBayniK(Y|lLj4P(PS6;VR{Qm9a|or6ot$|J zk?{s3-IxlJj6kX;CMJ>Cq=0`e-Wb!i-JQ9QY_;^POF%k18jr~*b}5{lQJ4&Zo04AU z-xW$B93L3og(COQzt)?)WG}u6q%bPC)B^^rr~IV?x{pCFpNX_B5L;W0_Oh2XiG{iE z-o5KEY408p40>F4I;*7!f?&@h4uV1)&|iwI8hI|UWy@~~%h0%UXHG?Pd$H}g+hVDF zl!U>!4&O{=ASk{nXS?vQ$<2q6^o8_4M9LN}c@OaI6nf zlD_!r4nsKQbhA0WxpT6g=~yH@V2JKpTp?516t@m>iB2`gh4Fb;0%gh)Rx2i;virX} zh(lotH-)=LfB13o*30Q$w(!;Y8+rNiWrCZVo8X^Hm(I4A&!5xF>Tj5~1(`Euf*YHf zvIMUMrzgPL@J{Lz@>G2Sp(Kv6t8k-KkK~@03!pCv7Xvu&qko+mdfAnMHAdS`v44aS z=~taUxQV&^xiw%h63sXX*BwrGbKZAWqAxTbR(k#%zi#j_<#tpsnYMj9?8Fpra8VDT zu6KL_^-i3I9;jZgd|9x9PI>sFGNx+IgOpKuL{2PzNRm4*`O|HP=DxLBU#=ucyPZAG z(6wc6?w4&O(7b`F>j^0D4*=@>T4IfqeXe@)fAaQbwrGbP2t>#FR=yK*11xjwXi}awsn_phYRo)cNn$z;(Mu~Fw{e6nn=ATHZ z9-NiTlsS#fd6D4PGgKu-QlqZ6u5Q!r)3nps#?J^y9OX()lpTDN$P540)z#HIAzYUH z418D5-V8anJXTJ+`Aq1L@wRiC7Yb;LysLVb;VHf{2#eGmaUJgGdM*X9GgTFpI%nP< z)36t-=|aH+abHgm_7jl`PgTq%L_sQD=uBOy#b`c-Mbj( zz~iLX6zONhyaxww$L*U1##6@uvw3D01FJ?v$Ip?QQml(3kIoAkr=b1d|FKR&5SE!HsC^oJ0 z?_p>j;1Msj`Cw<)roARBM;07WKPCUPt77$hGTH(_w3ENAU7mHK5I9bGcz7B)-^|XU zI8=ZwO4rC7Hs(FsO757SpO@ch#D#vCTPqY20DzM4e@6-&7}w2!AKnP@HXYWDI^*vk^@g#j zeR&g6cv_L%d-lJy`|yitiu{$fwx}~x066C@6_=1Gt9v+gP`8F@Np7Orhhw6g5%Pia zFc7B>)g^JHP9|_WvDbt?DI9k%joy$Gxw8LVucR3=>TEazR7|GF?L%7N7?=i4|DaCh zyMT)*cYoc*@5NHm?Tc>hAv3rn;bxZ-#_nn{b-xf(THi|uwCTC3_PdTWV!3%;gOvnfosIXYNC~iRa?_iQsvn*8*|ciOsCNLol>U zFzB@E(f+G1Lt3ZF?Ck6k93GuR;XQ>gKx?A&jto~RBcEDjAq9SBJaBe2eE60u)S+Z) zc4dVP`r=e`!T$HRCtJ>$;G<4dxEBz)JK?S7>t(n?cp$_1(&Xdw+w!qDZK+6lvR$K& zzmUq%XeoJdYi2)AJr*-4hBx;slG^UGI_}90%Q(o-rZQzfM;&GVnFrq1y zy@0B*x;G<5pvAMCBimZ*&Sd#(bT%>%?t)+0Lvl`UfB1UBoBl>#=-v6A46RVmy(?>IxWnpE?*kv%xVpP`fht4IL z?8&D;uW&&b<43nYioH_a5z4SQ1N?%~881b_oWH2sr1IMybfC*uHcJDj#m?ty3#_2F zfrreq|L#<1o$%REz0}vz0VgaRP4wKBURj=WbWU3hZr3Vjx%fhPt8kR@#&i-=WaX?A zM(KlZ6+WsAeekR#JI92brmZdMhUtyxSbCrJzCPV_MIk5vnr_STBL_YFQ~mGW+Gy1U z&9u{P+TFXO>tFa1t5lXVjrnsi-2L7_MN7q$*+3ciXb3$I@cxH zWpm!q9KA@|HZR!hw{YU(M6Q7{w?Mm8czUF+kRi}m{;>JLEE7X1{3`tL04nvj`a5&kEYU#`v^pg|p z!ZkZO_NxBP_as!zr?azKzV8c=R|5pIc~gT&yM7tFVO^%O%@ki6Wq9Gl&{(`11H11p zs@1tq?6QR=28wlmwi@HbjRi``*{>%Jfc)f(&;is6nc(#|{5+aY?nwEzj!5#~t>Ux` zMF8L19s|2hSu3TO1htyKwzcKmqjT)E0_pgOE$y>Df2wY^k7S@&WB+cy6+NvWSw>T( z*Pgnm9!M{6MgWo4_`9_QNz`9Qzw?u7cug-Eq3c=VCY|pbx;)^c6h~lCe=k{+!uSmw z*(Y-4_L-_vMPF0jet);d`4yjYuoNKh?iQ$#*{r+$T~oT8YEjgG-do`jKg|}}FrPad z9+)W)9~qnn#-v0StrBYY4qcmky0_f~QK~v*Ky%wE8gM@{w`97_0}8R4+gb9c-wE+_ zacGixk-Kz2EwI+vpk;HYqQLUpipOZL)^;5RLi_A@>qb4EBtLR+_=}?C?0)hx)q%71 zRoNit-z`yFF@nl7J1^^=zo@PXAP!!qtch)Sj!uie0{OjETL5<^nBM8w^HTR!|Id?3 zhK5!gOLVauDGM*6Z=)UpHA^o1gv1n)Kx9IL;m0xJD z{&Qy})Xm`J*NfRaC33y=(0)gRo00F275Q61Q^rF<=3u};9{?NEO5gUF|GsP6`TF0D z>H>*36@^x}Y7eNNT;$K<+SndNX5ArY96jnsji3HG9jUIap30uNXbRhG3EKPc#(U@V zj8lb6<_eJ5GTXG-I@Nr#QYM`};q<>@wKjLAElA3oCmiY@Mte)$0?P<4qSg@p$F5tB zrf$%qZh<7E?RR$m$1?FZ1Uwad{at=0U}5Fb*KsRG=?Wg-CB!YG>j8Abi^LpgjeuFq zZ#qOZ&65+U>a@}eB!IZ_Q{_3HAnej_4@Wm)eb#A3w?AE2X!~kfcZhXRo8CZUk%xH& zNoRK^EUY<%QQXw(1)fP-jc?M<4zbHEqbolB!!s#X?Dp%^jsKqdcE41qZ2l}@5r657 z{GfUtfYYGWwa|3?zv|l<0Ujc8Q)ewqYe4NnwXw0Whq4K*sf|R9;M%pJ*oK*B+p`zs z*=`CAp*iuso2lv0dJ5WcNZ@XjfYg6Dj6H3o#i*vPzB1W{Zy<#8YJsdK#GYmDF=#`G zv@Ym^tQ5?h4EhhiBCV*vANd}jCjpDOA~($lS`MZES_&16K$qy3Fs_1a{QYLVu?84j z)i=gLK}Ju_^)WgvKLOzsU-5Yg3qIu)J!5Xy>rd3+*(Ec5qJo^R zeD-ccApov)RxpG5A=gw!aXeJ&7IB5MO-*;(FNo2zj`)#mT%vZPqSf>X()3FWiM z)5wXFJ(tRMZAH{u2-t!Ot&`7j>Aoa#D~nS2*47th4h}7+Q?us>P?7UvHZmtYSB?oFx#uHsoa5KcZp)Q-)FttqLUT_(IXM}; z$Mm9(eRcTulaOtY1uaqqSyz=9E@6$Wj}lLAz2nW-6^!ep(4`Hy;qqr zD&|)^e{~@RTZeBPA3G|mp3~c& zk@M{rW3Ce!@A-SX*0DWS=EJ%*d;Zpe?;Izui=PHhqx##a`n!Yp`@N}P$B3&=aQUm3kFFYCgjtfHlL&hnV_tbH4F$&C(i zfCUh~%P3nOm>A%UGpFav5Qr9_1{=(w4*?!nM!qbaPK?TUzVOlW`@B7%c7+?8YQO29 zjmkwhl|Ycrn!0hZ4fH}82a{7vyY3#VuaTL#U9+*@jg2@WK2Ua#b!6I%HR zh8EKTJI5OO(E;Z-Z7z&$h+0SOOdp(%IygDLz&xw;eCb5Crnu+FkLVIf)a|~A{HwuG z@J$6U1s#abku744aow7@oG_m@^f;+dHS5Ik?b)Hb zFGy(>LXX$^*QUoQ14x=b5~JKHBDK(%FB$#^w^D=`!r^DnaqZuzvB%>vHcm9eirciexwdZ?niPnwvI%cnYMU}{L-a8xiFx!<&$phjmpdfLoU^X&yVa79 zMGR8uBM+Nq-@50_;{J4T>E;-f8~J<7jdLiQZ~&s@nQ%&+%WNMX96ToczU?FH?dd69 z{_*1j8ENS-n8-<+dxfwAKZ4)K@>W0w95JV33kx2(QB_$livRh9{{iyaZ&j$NRv26D zx+XQZt^OMK1jdl%S}EArI(^!yoMe@1K8#mzu{O`{z<$(Ir$rUkBDoq^&?Z()f>x63 zBF9$$VaM%mc0=n|dX3)zdPfbT6`S=ILs`1=7_&d1lF15J=_Gv3M>1z?03^R`3d}bz zrdjch;fl{q>gzlnm1>s~g#`+eVDSOhZ3XoXds*8YEq-iwopT?YTnURfz~dl)XHS0D zeW>;RYpma4otf%{-=WJU#@=uTW56&-odV5jdy2v_hr++s;Y&30ZX%5cbsN@fl zTf3PVaBTnVFx!Wsz_Z<(bZi9I^bcE~toi+D+`-n~*7ly_FXu-Q5t=X@Z=WV9g!&r7 zP?!d8TOjfYG7*617>TYG>9=>YzvQ2qV%lyQmfCUoG!3&k94iIqfkTDodNbEM|ARuv zVkt3LDQqB2$`sqP@1V@#VU7vzpO?a3ch?r%)^MCbv1C7iAlpz6rQgo>EFj1B$9rOh z!miA_QuHq$aeLkRNlnbHq4)THy{fphc=NWs_T`!lP2>EerL(Ks$=|gSdn_q^0j8_5OmoK#=I+u1L%4;d%?jYZyiQS>>`b zCwdZSXrCzMn$NjMSns!wOyG%h9ut(8ky1x-1N=Z#OQ<0)PDh5+t~qQ4mctKe$8vDk6=Hc($h%MjQa6Lwb=Oj_0QMkTLK^6 zzCES!=ioY56>kwE{1X2A3+R9J9N($H6^)D|P#CBh)>qHj`;0oci(78=T}w>mGb6~5 zfI3KrU@0YR_GFI*4o&E-bg|nt{mrV(aGe|c&9H>;;x3nH8Wv1lv8`9ng#RTR8__kG zAmVx!x8vMTkK!a8f3(fV%mC~MtCAI15<~n5HfL+R;y#W+`nL0f>u(iy?21<3unl@j zX3XX{NbEy>%wLO$w(nv>TbbyLDDTG#Rh_a}#$uEaL(TW>s6I#jK*a@iUW1Iv9Z z?ths(FG{Wd;t;__BFHbh^{>l_oagaf4J0{5?!K8_TVGl6f4n~Vf-*AjOZMEw0$o6i zDFg*S2n6czeNt@>*GaSJ!t9l(B1NQ|{KC@6uThDQ%hGJ3BkP zuh`5AW3Y3&FV*`Mvgx1#@Wh}e8JGw7_P7=2COIR62DF0ORZtkyVf)M=t8Zo4@a{65 z->kHli6UU!rdlo(M_VdlxMz_kqmgZBs^- zm?J=d*t7FXK7^OO9P4=fp2629jLypT#XpwTmG5+gSlCCoMS?1O;&P8xMM%QzoN8G? z!q{Q4xVU(0n`>{X{N)9d|JsCNf?rgamsbk>_sDN!Gm;NtzDaQk7-0dUl_g{OulvhA zW}^j;pPUyg)E|fhUOcCp*Y)WMw;g^w+!{PgAr9Gug*A02&fHx=?}}yF?~JRcs8qOh zIT<0H6K@7ckBR5sy4P(8NnPcp)~gf5h{l7#FxeG*w~1ctobx!1+Sp}iOAO_ShFDW$ zI{Px9B_fqd;Md~r2cavpHXz)en;$>ls0wFzFks32h=RsA$o|briKZ4mgO3X`YY`6~ zu~$~tFxtp}jImBxErwgSd^?+bzmMxy2$w73A#71|W(AoTEuxNr5MXv6Z_*kX&WGtW zqPdz51tpx&iv%YdL2UH&;{J?`nEQ1ysGd<-GS3W%#+>A%3vvz!leONQ>E(6y571Je zcniqgWF>=XQBaJpP+{)v=#{xWo?Nf2z$qJzsS}&X7bJlo|`i^Flnw{d3lqvqi{>; zx>=k61O!0mdAYud0+@m4AM0Dsn)LJg@>u6|V$)8vs899|DXZPK0qSHVYGbA)EG3C- zr+!x?2RXl$;e=aJzD<Yi2) z&R|Fz{P7#-%p`S1FokYYZ&Bkve}C^XiI}A7LDQ4Kea>cWCqG@>99j9^IFkUHyOZvTxX7A8i>m10^~Fqr`bB2NF?rhkT6EaXY!3|h`cLEJF) z?Kd~|2kI%A%Fq4cH2}6*gb;zEALm$ z5+LoF=w~{Nkq)z_XTscc?@rAYhZO!suGT)BYV{v?2zuy>c1}x6)1ai+SGQq)$Kj6Y z#1QO#gyfih`l9_@wmgt1awPdyi*$3Mby?${wsM9}R|6WjBrjhVoC=6Mt{NJ{9h8+f zuIPVYnNBw;F0-Olgh8C}>AAX#Y20SDpem7d2rV34I-OgEcn@~deLkHf3Xx+nG<^+& zk8KD0d+<=}i#y*B1nc(}|MR@_`XdW>_T7q0A1ns$N@f^IiFHN%ykt2xB!8(o|7#Ka zyYZ-*osEp6HM^Ucr5%0E3wEdRax9!@Fl3amZvr8OPmd_)??C*z#dT)Li4|h zh;`43WeQFm;pgYa$|Pu03;Mil$OexE;ilUIT`|*{>HJSw>8L2`(=9a(wsT+<3{Go! zqck#M)kFH*sn@XCTYrZ%&#*&Abhq}6eUpW&C9@xIZ&=?E{tq8VYO2t14g#rsf8m(s2w28M3yoddU*YYPo}bOzigdX>sAMy)5!3!ua<^Q=~c!8 zW%M@q+_=*mjm*uiX6_@dH_ZHtH`v-KAp{fI;Q&ib2ZL;WuWwwJg6Dg)e=|8G{hZ)N z)3I;=G=Fwk%zz}T9JOS?fDz3e|4I|wD$rX#bye)oJ=Rvo)-aCwccAo#rb^`V?a#OC zA~!vqPA8w`kd6|q!u)UP7!``8@s>=kY4c=01v{R>Y{y>K=?$pT&pxDVRsa# zs!%rmUE*xYQP`IaIt)6eX>auU8^?p{td`&(uT`Q3i*TU}=Z!csTblm@$EkQ}{spk$ zPIeT&4h`Xdu!=o1HLaBk!c!F{28_qwbrUKNhXJ`eW6a;DaAr>nGE-G@kksd zi^qOkV@V|R?G!}K_$kf8N%@Cex(q?Ri0ZS;<+DTepLsL8!#Yg++tq(_NDaEc z%(|YRA8Y}Gysu#xnh1U>Its#jA#SDv>9MC0%=?{#%-g-p>i4Jo#>J_q;Pt;HFI{5p zRUvY4ZiR<0GX^D%&<|LAv2PlMkc!ReKGY+?&p8|4;$s!&Jf zT0w`=?P0R6#xPx7B!CgspU)4 zy_n`sQoSO)r0xcBjuj@|R zPi(!kF3xrNbQISq=_86J)QoHZo(?sjVc(ReI!PnwL zysep<4={gOGiv8(o1l>wypROjY(3qO2n!7r7rL2~dU;SCapNn74@D1j42orl106}I zBpd?Chlnnxao`P5=X4`MW+N?hoDeSYerpQlKHAsNqC#iQUeL2ggX+XFhp_b;V)fy5 zE=1rKP>4q2XJX?Orz`;)9FmdEy#lH_@+hz-gLF%^w>CX?O9(y7BoZz#+zF4`JXoKK zx+rrZuUtBqVH(L%7L0L3s4@xrUUk<|1L|o8#Z(aD8DPMK72r~wirm;hap*gnLj`*O z5dh>$SjQU7NEB!GaVcad%#rZR)+u6;;&Vt5q;h5$QvBV3a$~19f~a>1LZFS`^TnqU zLqFZMN#bc=iJ46-*`Wsf@34~on(nzoM!cB}{Ks=zow9YPN#6SzRZqD*wJ3_=eFWl$ zjL~0zKNj?aG$XKp7hz0aNJP;BXH)5O^>vXu*Ep#F7i2mUpqsS4%K(_KbmvgI@O55) zMuY#ou+6P`@bd$uO(|Puq%ut7gag@}F+do?)0PtM8|94VSz;N;>6Z0$BqVi@naGkR^3z}sU&C&!w*C31FgsZopLfN5H!gDr21^ZiL}Cn1pPjB ztR6Lh>3tU+l{pD4)Hmn#ea3FmDDmXV{O@}fyIIEXKQ%U99r=Am>A>En5L6y&Kw{h| z3Q`aM16M}V$AqdYMI~09Z_K9h_)X^}zDcvA(QtgIb=sB0nlci^|M$ziq4{2cur*&F zpH1DUs0(G&;ev{U``$s-VgRo@>jy#l1uGD5M-s)SYo`j70|IW|1OTD7_s+m@#r)o2 z5jN1y&TeGoS6P{=&&+gd`0>um4Am11xdBIf02OdGh#GMB(TOG$GXP74U{U7NcUIW&X0Z7* ziWZ7&kDO4!1kmc~>A6$Vw$h@at~)E*$CNGSZwF9N)zHvTkjqf6^gJuec8T*ETcrgH zbYG!R;GeLjU`kmYX>DyS>}0t(?A1-w6-~{DyDJ(Le%#e8*z98qO;r;r04`n{ZjF`9 zagiA{+o>bLd_>HMCR9+egzFQd5TE zHa@+?X{$VADfOh_zhz}*(p-|yv-}hbOG=*O3>2GQs_On5NwR;BXzR>~A^r$3(d>i= zzh>TAgz~0L^d=-E^isoM2HZh{?p<14dq+Yhq-zJc0grRTHkXzp3}lCqWwot&OaKXq zpggkrcuA4GNUbCWB%>E4D596#Mga716$PE80dBTFM47||936JhNLf#1yve)w! z3duKX^+1n>zqJ5{hxGhPv>yVPQFIP1LSiZJi5j+k_P_DQNp;pk@>G6A8?V2~MWk($ z776#pY+|?}o!K|%{4oOHnzO5Zn;vlfkgV}8eBqpI$qz#m3+R3X;t)|>T<9*vi#}eU za8PXC;+PWOqFB)@(Z~lv%lnvFe)l;#FjlR}U^sF(=a{HMvLCx_c;dCoZB+k+*I&+? z!Y&6xjUeXb?UHhQ23;=pnkHRo&w!m8Q0xyot)U0MDL`%hVG&?f0!`P|GrY5RS-Vx5 zKEETC3KYxhd+k5)Fws!Z#RAK`rHVf!pKm^~hcgd3I0eRUW?yXT`$b&@#TfySh52pF zdNM9uY`VTiqS0!DdAhwr24F)E=pL$(P*>Vfw|B)Cf1$#M>q2g%5{2n6Bnpf#D(t3LOk03)-W&U3M~gu5J!Mqc}fg#Xpn? z>1GZo01*0XF0=fPoc!7@Z5+bdBYoQ%o#N&!f&Yh@WFj(YQV{nNsJMsK-EiGdCn6 zdRz!rU~J@u6$ssWC0g=Es3`AbpX$i{JL08L*W{ciC(C)F@GGU{XM@ z@;LEGKibu(`>cKzieR7x+G7PCU!!Ty^I%7kZc+r$_|udkkJH3PvO33NBq_7yLTWT9 z-orXVq~*jky6dslkVVQd7>GboDvJzov|8K6)hM(H-*_5f2Xl(>*CN$*k=?QP7v#IB z&HA>(4O*wg1bNY84WgNt94LUO2qLa(1%}VAw~3R2xfH~NPP7}MrqvZQWiqufNH1m> z)ej(+HENVBd55WqLt9JmG0F%8fDd-5aFw3K6R(9Hd(P}Pb4{70)JCGQ;Z{(T3%F}_ z#VkEkhz<1j?E;9gx!s)1&)wgDH27tyS+fBIPGfK24eCWnz{ z*V7c}AxszjSl`0pwzn0N0F^!KS0)fKCem_jH&H|2Dy^MloH5vF&qd^phSP%l-WT&j zo}T&rhi~ye5?9a0NmE~ap!R~wPUkSEM2Y~q?KW%Rgik<3Z=i4x@~o~wXgtB8_@YJf zwzg0#JymY<$?5qilZN1a7$MiZ(2r7Lw&kU1<)rBXLcQ=f`5M-t=gS=XyZiy> z3?{V^XhFtURvA&XEUV8zA`BZIHEzG8@tCafZ~Ff%1-wW^Mc**QiJIZX(;rpH`ACJL+l)TQhADeA(Gffrfx&t)@3Od zgn^BSl6kuYu5xsePJ?WB@lS+9Pt4u%er<`~@!?DlC75S-#deSchs;^c@erBC*Uu=y z-gs=qyyqH85I{%HrC0FGx6SwzjN@X;cR5XP>4yqBEA~r@+u5rNm!9i%`(0G7i$Vp0 zwAR81z@?Secac7PK-Uk#H}JBkt6Xwfx*mnWw^$|~i=%$X>G4S#8oTp0O?$96^>{%b z4!{UC?%3;YH}EX)h@FehnUSW&`03awi#5?zB5^X$N5plas(wg6Le5LT4G8a&)>ra>dw_T z->L{Md=Jvtux};qYQDVd%~9Q&7dX&Mff3kf)vpL{d@GtVI(RmCebjS@N5T^$MxJTl zVcAuWY6KzkLcSeN#$DU7`x@{_T3r2#6JXYNtW9*Ep~>s{=6H9vW{y~Tb)dJZK44dN z5XA{Xe%p`p9Xiv|&qKn1{>egLa zH|o+oiOMGXg>OuwvrX@4y=XnsUJ9?m=?KvUx6%}u-N}(3RflaBD$YGW5DiS z;h?>z&fdE5>_>=c&Gko$Pmk@VCEmQtrR|)K2JOYw51Z6%hG{L$1=d?~{>)P(ISofpe~y-XNTyS&47516C8=5`Y3fgJ98JpsWvAS~shV8*n|fUkQF_t3l_ zH87ZRnV^1c5Wv6kbHA3*%MGYq9!w!_sX>5yJ+HR;1pdE2Ig6Tjjm6yoXX60MVWelG JTdwUK{eJ~_&VK*^ literal 0 HcmV?d00001 diff --git a/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-riak-todo.yaml b/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-riak-todo.yaml new file mode 100644 index 000000000..958d29d04 --- /dev/null +++ b/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-riak-todo.yaml @@ -0,0 +1,46 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +name: "Node.JS Todo Application" +origin: "https://github.com/amirrajan/nodejs-todo/" +location: + jclouds:aws-ec2:us-west-1: + imageId: us-west-1/ami-c33cdd87 +services: +- type: org.apache.brooklyn.entity.nosql.riak.RiakCluster + initialSize: 2 + id: mycluster + brooklyn.config: + provisioning.properties: + osFamily: centos + minCores: 4 + minRam: 2048 +- type: org.apache.brooklyn.entity.webapp.nodejs.NodeJsWebAppService + id: nodejs-riak1 + name: "Node.JS" + brooklyn.config: + gitRepoUrl: + "https://github.com/bostko/nodejs-todo.git" + appFileName: server.js + appName: nodejs-todo + nodePackages: + - basho-riak-client + env: + NODE_ENV: production + RIAK_NODES: > + $brooklyn:component("mycluster").attributeWhenReady("riak.cluster.nodeListPbPort") + launch.latch: $brooklyn:component("mycluster").attributeWhenReady("service.isUp") \ No newline at end of file diff --git a/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-todo.yaml b/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-todo.yaml new file mode 100644 index 000000000..6aab1dbce --- /dev/null +++ b/examples/simple-web-cluster/src/main/resources/org/apache/brooklyn/demo/nodejs-todo.yaml @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +id: nodejs-todo-application +name: "Node.JS Todo Application" +origin: "https://github.com/amirrajan/nodejs-todo/" +locations: +- jclouds:softlayer:ams01 +services: +- type: org.apache.brooklyn.entity.nosql.redis.RedisStore + id: redis + name: "Redis" +- type: org.apache.brooklyn.entity.webapp.nodejs.NodeJsWebAppService + id: nodejs + name: "Node.JS" + brooklyn.config: + gitRepoUrl: + "https://github.com/grkvlt/nodejs-todo/" + appFileName: server.js + appName: nodejs-todo + nodePackages: + - express + - ejs + - jasmine-node + - underscore + - method-override + - cookie-parser + - express-session + - body-parser + - cookie-session + - redis + - redis-url + - connect + env: + REDISTOGO_URL: > + $brooklyn:formatString("redis://%s:%d/", + component("redis").attributeWhenReady("host.subnet.hostname"), + component("redis").attributeWhenReady("redis.port")) + launch.latch: $brooklyn:component("redis").attributeWhenReady("service.isUp") \ No newline at end of file diff --git a/examples/simple-web-cluster/src/main/resources/visitors-creation-script.sql b/examples/simple-web-cluster/src/main/resources/visitors-creation-script.sql new file mode 100644 index 000000000..2422f8f00 --- /dev/null +++ b/examples/simple-web-cluster/src/main/resources/visitors-creation-script.sql @@ -0,0 +1,41 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. +-- +create database visitors; +use visitors; + +# not necessary to create user if we grant (and not supported in some dialects) +# create user 'brooklyn' identified by 'br00k11n'; + +grant usage on *.* to 'brooklyn'@'%' identified by 'br00k11n'; + +# ''@localhost is sometimes set up, overriding brooklyn@'%', so do a second explicit grant +grant usage on *.* to 'brooklyn'@'localhost' identified by 'br00k11n'; + +grant all privileges on visitors.* to 'brooklyn'@'%'; + +flush privileges; + +CREATE TABLE MESSAGES ( + id BIGINT NOT NULL AUTO_INCREMENT, + NAME VARCHAR(30) NOT NULL, + MESSAGE VARCHAR(400) NOT NULL, + PRIMARY KEY (ID) + ); + +INSERT INTO MESSAGES values (default, 'Isaac Asimov', 'I grew up in Brooklyn' ); diff --git a/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java b/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java new file mode 100644 index 000000000..2dfbb8113 --- /dev/null +++ b/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.brooklyn.demo; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.sensor.Enricher; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.mgmt.rebind.RebindOptions; +import org.apache.brooklyn.core.mgmt.rebind.RebindTestFixture; +import org.apache.brooklyn.enricher.stock.Propagator; +import org.apache.brooklyn.entity.proxy.nginx.NginxController; +import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster; +import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8Server; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.HttpTestUtils; +import org.apache.brooklyn.test.WebAppMonitor; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.time.Duration; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.entity.database.mysql.MySqlNode; +import org.apache.brooklyn.entity.group.DynamicCluster; +import org.apache.brooklyn.entity.java.JavaEntityMethods; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.policy.enricher.HttpLatencyDetector; + +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + + +public class RebindWebClusterDatabaseExampleAppIntegrationTest extends RebindTestFixture { + + private static final Logger LOG = LoggerFactory.getLogger(RebindWebClusterDatabaseExampleAppIntegrationTest.class); + + private Location origLoc; + private List webAppMonitors = new CopyOnWriteArrayList(); + private ExecutorService executor; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + origLoc = origManagementContext.getLocationRegistry().resolve("localhost"); + executor = Executors.newCachedThreadPool(); + webAppMonitors.clear(); + } + + @AfterMethod(alwaysRun=true) + @Override + public void tearDown() throws Exception { + for (WebAppMonitor monitor : webAppMonitors) { + monitor.terminate(); + } + if (executor != null) executor.shutdownNow(); + super.tearDown(); + } + + @Override + protected StartableApplication createApp() { + StartableApplication result = origManagementContext.getEntityManager().createEntity(EntitySpec.create(StartableApplication.class) + .impl(WebClusterDatabaseExampleApp.class) + .configure(DynamicCluster.INITIAL_SIZE, 2)); + return result; + } + + private WebAppMonitor newWebAppMonitor(String url, int expectedResponseCode) { + WebAppMonitor monitor = new WebAppMonitor(url) +// .delayMillis(0) FIXME Re-enable to fast polling + .expectedResponseCode(expectedResponseCode) + .logFailures(LOG); + webAppMonitors.add(monitor); + executor.execute(monitor); + return monitor; + } + + @Test(groups="Integration") + public void testRestoresSimpleApp() throws Exception { + origApp.start(ImmutableList.of(origLoc)); + + assertAppFunctional(origApp); + + String clusterUrl = checkNotNull(origApp.getAttribute(WebClusterDatabaseExampleApp.ROOT_URL), "cluster url"); + WebAppMonitor monitor = newWebAppMonitor(clusterUrl, 200); + + newApp = rebind(RebindOptions.create().terminateOrigManagementContext(true)); + assertAppFunctional(newApp); + + // expect no failures during rebind + monitor.assertNoFailures("hitting nginx url"); + monitor.terminate(); + } + + private void assertAppFunctional(StartableApplication app) throws Exception { + // expect standard config to (still) be set + assertNotNull(app.getConfig(WebClusterDatabaseExampleApp.WAR_PATH)); + assertEquals(app.getConfig(WebClusterDatabaseExampleApp.USE_HTTPS), Boolean.FALSE); + assertNotNull(app.getConfig(WebClusterDatabaseExampleApp.DB_SETUP_SQL_URL)); + + // expect entities to be there + MySqlNode mysql = (MySqlNode) Iterables.find(app.getChildren(), Predicates.instanceOf(MySqlNode.class)); + ControlledDynamicWebAppCluster web = (ControlledDynamicWebAppCluster) Iterables.find(app.getChildren(), Predicates.instanceOf(ControlledDynamicWebAppCluster.class)); + final NginxController nginx = (NginxController) Iterables.find(web.getChildren(), Predicates.instanceOf(NginxController.class)); + DynamicWebAppCluster webCluster = (DynamicWebAppCluster) Iterables.find(web.getChildren(), Predicates.instanceOf(DynamicWebAppCluster.class)); + Collection appservers = web.getMembers(); + assertEquals(appservers.size(), 2); + String clusterUrl = checkNotNull(app.getAttribute(WebClusterDatabaseExampleApp.ROOT_URL), "cluster url"); + String dbUrl = checkNotNull(mysql.getAttribute(MySqlNode.DATASTORE_URL), "database url"); + final String expectedJdbcUrl = String.format("jdbc:%s%s?user=%s\\&password=%s", dbUrl, WebClusterDatabaseExampleApp.DB_TABLE, + WebClusterDatabaseExampleApp.DB_USERNAME, WebClusterDatabaseExampleApp.DB_PASSWORD); + + // expect web-app to be reachable, and wired up to database + HttpTestUtils.assertHttpStatusCodeEventuallyEquals(clusterUrl, 200); + for (Entity appserver : appservers) { + String appserverUrl = checkNotNull(appserver.getAttribute(Tomcat8Server.ROOT_URL), "appserver url of "+appserver); + + HttpTestUtils.assertHttpStatusCodeEventuallyEquals(appserverUrl, 200); + assertEquals(expectedJdbcUrl, appserver.getConfig(JavaEntityMethods.javaSysProp("brooklyn.example.db.url")), "of "+appserver); + } + + WebAppMonitor monitor = newWebAppMonitor(clusterUrl, 200); + + // expect auto-scaler policy to be there, and to be functional (e.g. can trigger resize) + AutoScalerPolicy autoScalerPolicy = (AutoScalerPolicy) Iterables.find(webCluster.policies(), Predicates.instanceOf(AutoScalerPolicy.class)); + + autoScalerPolicy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 3); + EntityAsserts.assertGroupSizeEqualsEventually(web, 3); + final Collection webMembersAfterGrow = web.getMembers(); + + for (final Entity appserver : webMembersAfterGrow) { + Asserts.succeedsEventually(MutableMap.of("timeout", Duration.TWO_MINUTES), new Runnable() { + @Override public void run() { + String appserverUrl = checkNotNull(appserver.getAttribute(Tomcat8Server.ROOT_URL), "appserver url of "+appserver); + HttpTestUtils.assertHttpStatusCodeEquals(appserverUrl, 200); + assertEquals(expectedJdbcUrl, appserver.getConfig(JavaEntityMethods.javaSysProp("brooklyn.example.db.url")), "of "+appserver); + Asserts.assertEqualsIgnoringOrder(nginx.getAttribute(NginxController.SERVER_POOL_TARGETS).keySet(), webMembersAfterGrow); + }}); + } + + // expect enrichers to be there + Iterables.find(web.enrichers(), Predicates.instanceOf(HttpLatencyDetector.class)); + Iterable propagatorEnrichers = Iterables.filter(web.enrichers(), Predicates.instanceOf(Propagator.class)); + assertEquals(Iterables.size(propagatorEnrichers), 3, "propagatorEnrichers="+propagatorEnrichers); + + // Check we see evidence of the enrichers having an effect. + // Relying on WebAppMonitor to stimulate activity. + EntityAsserts.assertAttributeEqualsEventually(app, WebClusterDatabaseExampleApp.APPSERVERS_COUNT, 3); + EntityAsserts.assertAttributeChangesEventually(web, DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW); + EntityAsserts.assertAttributeChangesEventually(app, DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW); + EntityAsserts.assertAttributeChangesEventually(web, HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_MOST_RECENT); + EntityAsserts.assertAttributeChangesEventually(web, HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_IN_WINDOW); + + // Restore the web-cluster to its original size of 2 + autoScalerPolicy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 2); + EntityAsserts.assertGroupSizeEqualsEventually(web, 2); + + final Entity removedAppserver = Iterables.getOnlyElement(Sets.difference(ImmutableSet.copyOf(webMembersAfterGrow), ImmutableSet.copyOf(web.getMembers()))); + Asserts.succeedsEventually(new Runnable() { + @Override public void run() { + assertFalse(Entities.isManaged(removedAppserver)); + }}); + + monitor.assertNoFailures("hitting nginx url"); + monitor.terminate(); + } +}