diff --git a/.travis.yml b/.travis.yml index d98a2e3..4264227 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ script: - cfy blueprints validate -p ec2-blueprint.yaml - cfy blueprints validate -p openstack-blueprint.yaml - cfy blueprints validate -p openstack-nova-net-blueprint.yaml + - cfy blueprints validate -p openstack-haproxy-blueprint.yaml - cfy blueprints validate -p singlehost-blueprint.yaml - cfy blueprints validate -p softlayer-blueprint.yaml - cfy blueprints validate -p cloudstack-blueprint.yaml @@ -24,4 +25,3 @@ script: - cfy local init -p local-blueprint.yaml - cfy local execute -w install - cfy local execute -w uninstall - diff --git a/inputs/openstack-haproxy.yaml.template b/inputs/openstack-haproxy.yaml.template new file mode 100644 index 0000000..6755803 --- /dev/null +++ b/inputs/openstack-haproxy.yaml.template @@ -0,0 +1,3 @@ +image: '' +flavor: '' +agent_user: '' diff --git a/openstack-haproxy-blueprint.yaml b/openstack-haproxy-blueprint.yaml new file mode 100644 index 0000000..0e707f7 --- /dev/null +++ b/openstack-haproxy-blueprint.yaml @@ -0,0 +1,334 @@ +########################################################### +# This Blueprint installs the nodecellar application behind +# an haproxy instance on an openstack cloud environment. +########################################################### + +tosca_definitions_version: cloudify_dsl_1_0 + +imports: + - http://www.getcloudify.org/spec/cloudify/3.2m5/types.yaml + - http://www.getcloudify.org/spec/openstack-plugin/1.2m5/plugin.yaml + - http://www.getcloudify.org/spec/diamond-plugin/1.2m5/plugin.yaml + - types/nodecellar.yaml + - types/haproxy.yaml + +##################################################################################### +# inputs section allows the user to use same +# blueprint for creating different deployments, each one +# with its own parameters. +# to specify deployment inputs run: +# - cfy deployments create -b -d -i inputs.yaml +##################################################################################### + +inputs: + + image: + description: > + Image to be used when launching agent VM's + + flavor: + description: > + Flavor of the agent VM's + + agent_user: + description: > + User for connecting to agent VM's + +node_types: + + ########################################################### + # We define a type that inherits openstack's default + # server, and adds monitoring capabillities on top of it. + ########################################################### + + nodecellar.nodes.MonitoredServer: + derived_from: cloudify.openstack.nodes.Server + properties: + cloudify_agent: + default: + user: { get_input: agent_user } + server: + default: + image: { get_input: image } + flavor: { get_input: flavor } + interfaces: + + ########################################################### + # We are infact telling cloudify to install a diamond + # monitoring agent on the server. + # + # (see https://github.com/BrightcoveOS/Diamond) + ########################################################### + + cloudify.interfaces.monitoring_agent: + install: + implementation: diamond.diamond_agent.tasks.install + inputs: + diamond_config: + default: + interval: 1 + start: diamond.diamond_agent.tasks.start + stop: diamond.diamond_agent.tasks.stop + uninstall: diamond.diamond_agent.tasks.uninstall + + ########################################################### + # Adding some collectors. These collectors are necessary + # for the Cloudify UI to display the deafult metrics. + ########################################################### + + cloudify.interfaces.monitoring: + start: + implementation: diamond.diamond_agent.tasks.add_collectors + inputs: + collectors_config: + default: + CPUCollector: {} + MemoryCollector: {} + LoadAverageCollector: {} + DiskUsageCollector: + config: + devices: x?vd[a-z]+[0-9]*$ + NetworkCollector: {} + +node_templates: + + mongod_host: + type: nodecellar.nodes.MonitoredServer + relationships: + + ########################################################### + # Attaching the mongo security group to the mongo host + ########################################################### + + - target: mongod_security_group + type: cloudify.openstack.server_connected_to_security_group + + nodejs_host: + type: nodecellar.nodes.MonitoredServer + + ########################################################### + # Setting the nodejs_host initial number of instances to 2. + # The default values for instances.deploy is 1. + ########################################################### + + instances: + deploy: 2 + + relationships: + + ########################################################### + # Attaching the nodecellar security group to + # the nodecellar host + ########################################################### + + - target: nodecellar_security_group + type: cloudify.openstack.server_connected_to_security_group + + haproxy_frontend_host: + type: nodecellar.nodes.MonitoredServer + relationships: + + ########################################################### + # Attaching a floating ip to the haproxy frontend host + ########################################################### + + - type: cloudify.openstack.server_connected_to_floating_ip + target: frontend_floatingip + + ########################################################### + # Attaching the haproxy frontend security group to + # the haproxy frontend host + ########################################################### + + - type: cloudify.openstack.server_connected_to_security_group + target: haproxy_frontend_security_group + + mongod: + type: nodecellar.nodes.MongoDatabase + properties: + port: 27017 + interfaces: + cloudify.interfaces.lifecycle: + + ########################################################### + # The MongoDBCollector depends on a python library called + # pymongo. We install this library in the 'configure' + # lifecycle hook of this node. + ########################################################### + + configure: scripts/mongo/install-pymongo.sh + + ########################################################### + # Notice that this node defines an additional collector + # to collect data on the MongoDB. + ########################################################### + + cloudify.interfaces.monitoring: + start: + implementation: diamond.diamond_agent.tasks.add_collectors + inputs: + collectors_config: + MongoDBCollector: + config: + hosts: localhost:27017 + relationships: + - type: cloudify.relationships.contained_in + target: mongod_host + + nodejs: + type: nodecellar.nodes.NodeJSServer + relationships: + - type: cloudify.relationships.contained_in + target: nodejs_host + + nodecellar: + type: nodecellar.nodes.NodecellarApplicationModule + properties: + port: 8080 + relationships: + + ################################ + # Setting the mongo connection + ################################ + + - type: node_connected_to_mongo + target: mongod + + ################################ + # Setting the nodejs connection + ################################ + + - type: node_contained_in_nodejs + target: nodejs + + ################################ + # Setting the haproxy connection + ################################ + + - type: app_connected_to_haproxy + target: haproxy + + + ######################################## + # Note: only ubuntu haproxy installation + # is supported. + ######################################## + + haproxy: + type: haproxy.nodes.Proxy + properties: + + ########################################################### + # This is the port that will be used by haproxy. + ########################################################### + frontend_port: 80 + + ########################################################### + # This is the port that will be used by haproxy to display + # statistics. + ########################################################### + statistics_port: 9000 + + backend_app_port: { get_property: [ nodecellar, port ] } + + relationships: + - target: haproxy_frontend_host + type: cloudify.relationships.contained_in + interfaces: + + ########################################################### + # Notice that this node defines an additional collector + # to collect data on the haproxy instance. + # Also notice that in the URL, port 9000 is used. If you + # choose to change this port on the haproxy node template, + # this URL should be updated as well. + ########################################################### + + cloudify.interfaces.monitoring: + start: + implementation: diamond.diamond_agent.tasks.add_collectors + inputs: + collectors_config: + HAProxyCollector: + config: + url: http://127.0.0.1:9000/haproxy_stats;csv + + ########################################################### + # A security group to enable access to the nodejs host + # using the port of the nodecellar application. + # + # This security group will be attached to the nodejs_host + ########################################################### + + nodecellar_security_group: + type: cloudify.openstack.nodes.SecurityGroup + properties: + security_group: + name: nodecellar_security_group + rules: + - remote_ip_prefix: 0.0.0.0/0 + port: { get_property: [ nodecellar, port ] } + + + ########################################################### + # A security group to enable access to the mongo host + # using the port of the mongo node. + # + # We need this so that the nodecellar application can + # comminicate with MongoDB, since they are running on + # different hosts. + ########################################################### + + mongod_security_group: + type: cloudify.openstack.nodes.SecurityGroup + properties: + security_group: + name: mongod_security_group + rules: + - remote_ip_prefix: 0.0.0.0/0 + port: { get_property: [ mongod, port ] } + - remote_ip_prefix: 0.0.0.0/0 + port: 28017 + + ########################################################### + # A security group to enable access to the haproxy frontend + # host using the haproxy frontend_port property. + # In addition, we open the statistics port (9000) of haproxy + # + # This security group will be attached to the + # haproxy_frontend_host + ########################################################### + + haproxy_frontend_security_group: + type: cloudify.openstack.nodes.SecurityGroup + properties: + security_group: + name: haproxy_frontend_security_group + rules: + - remote_ip_prefix: 0.0.0.0/0 + port: { get_property: [ haproxy, frontend_port ] } + - remote_ip_prefix: 0.0.0.0/0 + port: { get_property: [ haproxy, statistics_port ]} + + ########################################################### + # A floating ip to be attached to the haproxy frontend host, + # since eventually we want to be able to access the application + # from any machine, on any network. + ########################################################### + + frontend_floatingip: + type: cloudify.openstack.nodes.FloatingIP + + +########################################################### +# This outputs section exposes the application endpoint. +# You can access it by running: +# - cfy deployments -d outputs +########################################################### + +outputs: + endpoint: + description: Web application endpoint + value: + ip_address: { get_attribute: [ frontend_floatingip, floating_ip_address ] } + port: { get_property: [haproxy, frontend_port] } diff --git a/resources/haproxy/haproxy.cfg.template b/resources/haproxy/haproxy.cfg.template new file mode 100644 index 0000000..38dc7c3 --- /dev/null +++ b/resources/haproxy/haproxy.cfg.template @@ -0,0 +1,38 @@ +global + daemon + maxconn {{global_maxconn}} + +defaults + mode {{mode}} + timeout connect {{timeout_connect}} + timeout client {{timeout_client}} + timeout server {{timeout_server}} + +frontend {{frontend_id}} + bind *:{{frontend_port}} + default_backend {{default_backend}} + +backend {{default_backend}} +{% for id, backend in backends.iteritems() %} + server {{ id }} {{ backend.address }}:{{ backend.port }} maxconn {{ backend.maxconn }} +{% endfor %} + +listen stats 0.0.0.0:{{statistics_port}} + mode http + balance + timeout client 5000 + timeout connect 4000 + timeout server 30000 + + #This is the virtual URL to access the stats page + stats uri /haproxy_stats + + #Authentication realm. This can be set to anything. Escape space characters with a backslash. + stats realm HAProxy\ Statistics + + #The user/pass you want to use. Change this password! + stats auth admin:password + + #This allows you to take down and bring up back end servers. + #This will produce an error on older versions of HAProxy. + stats admin if TRUE diff --git a/scripts/haproxy/haproxy.py b/scripts/haproxy/haproxy.py new file mode 100644 index 0000000..203e0d5 --- /dev/null +++ b/scripts/haproxy/haproxy.py @@ -0,0 +1,127 @@ +############################################################################### +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +import os +import subprocess +import tempfile +from contextlib import contextmanager + +from jinja2 import Template + +from cloudify_rest_client import exceptions as rest_exceptions +from cloudify import ctx +from cloudify.state import ctx_parameters as inputs +from cloudify import exceptions +from cloudify import utils + + +CONFIG_PATH = '/etc/haproxy/haproxy.cfg' +TEMPLATE_RESOURCE_NAME = 'resources/haproxy/haproxy.cfg.template' + + +def configure(subject=None): + subject = subject or ctx + + ctx.logger.info('Configuring HAProxy.') + template = Template(ctx.get_resource(TEMPLATE_RESOURCE_NAME)) + + ctx.logger.debug('Building a dict object that will contain variables ' + 'to write to the Jinja2 template.') + + config = subject.node.properties.copy() + config.update(dict( + frontend_id=subject.node.name, + backends=subject.instance.runtime_properties.get('backends', {}))) + + ctx.logger.debug('Rendering the Jinja2 template to {0}.'.format(CONFIG_PATH)) + ctx.logger.debug('The config dict: {0}.'.format(config)) + + with tempfile.NamedTemporaryFile(delete=False) as temp_config: + temp_config.write(template.render(config)) + + _run('sudo /usr/sbin/haproxy -f {0} -c'.format(temp_config.name), + error_message='Failed to Configure') + + _run('sudo mv {0} {1}'.format(temp_config.name, CONFIG_PATH), + error_message='Failed to write to {0}.'.format(CONFIG_PATH)) + + +def add_backend(port, maxconn, backend_address=None): + with _backends_update() as backends: + backends[ctx.source.instance.id] = { + 'address': backend_address or ctx.source.instance.host_ip, + 'port': port, + 'maxconn': maxconn + } + + +def remove_backend(): + with _backends_update() as backends: + backends.pop(ctx.source.instance.id, None) + + +@contextmanager +def _backends_update(): + backends = ctx.target.instance.runtime_properties.get('backends', {}) + yield backends + ctx.target.instance.runtime_properties['backends'] = backends + # being explict because errors in unlink are ignored and + # not retried without being explicit. + # also, this way, we make sure that configure/reload + # are only called with a fully update configuration + try: + ctx.target.instance.update() + configure(subject=ctx.target) + _service('reload') + except rest_exceptions.CloudifyClientError as e: + if 'conflict' in str(e): + # cannot 'return' in contextmanager + ctx.operation.retry( + message='Backends updated concurrently, retrying.', + retry_after=1) + else: + raise + + +def start(): + _service('start') + + +def stop(): + _service('stop') + + +def _service(state): + _run('sudo service haproxy {0}'.format(state), + error_message='Failed setting state to {0}'.format(state)) + + +def _run(command, error_message): + runner = utils.LocalCommandRunner(logger=ctx.logger) + try: + runner.run(command) + except exceptions.CommandExecutionException as e: + raise NonRecoverableError('{0}: {1}'.format(error_message, e)) + + +def _main(): + invocation = inputs['invocation'] + function = invocation['function'] + args = invocation.get('args', []) + kwargs = invocation.get('kwargs', {}) + globals()[function](*args, **kwargs) + + +if __name__ == '__main__': + _main() diff --git a/scripts/haproxy/install-ubuntu.sh b/scripts/haproxy/install-ubuntu.sh new file mode 100644 index 0000000..76e3613 --- /dev/null +++ b/scripts/haproxy/install-ubuntu.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +ctx logger info "Installing HAProxy" +ctx logger debug "${COMMAND}" + +sudo apt-get update +sudo apt-get -y install haproxy + +sudo /bin/sed -i s/ENABLED=0/ENABLED=1/ /etc/default/haproxy + +ctx logger info "Installed HAProxy" diff --git a/types/haproxy.yaml b/types/haproxy.yaml new file mode 100644 index 0000000..787dd0b --- /dev/null +++ b/types/haproxy.yaml @@ -0,0 +1,99 @@ +########################################################### +# This file containes node type / relationship definitions +# for installing, configuring and managing an haproxy +# instance. +########################################################### + +node_types: + + haproxy.nodes.Proxy: + derived_from: cloudify.nodes.ApplicationServer + properties: + frontend_port: + description: The port for HAProxy to listen on. + type: integer + default: 80 + statistics_port: + description: The port HAProxy will listen on for statistics requests. + type: integer + default: 9000 + global_maxconn: + description: Max number of connections at a time. + type: integer + default: 256 + mode: + description: > + The protocol that will act as a proxy for. + type: string + default: http + timeout_connect: + description: > + Set the maximum time to wait for a connection attempt to a + server to succeed. + type: integer + default: 5000 + timeout_client: + description: Set the maximum inactivity time on the client side. + type: integer + default: 50000 + timeout_server: + description: Set the maximum inactivity time on the server side. + type: integer + default: 50000 + default_backend: + description: > + The group name you choose for your backend targets. + default: servers + backend_app_port: + description: > + The port that your application listens on. + (This is the port that you will open on your backend application servers.) + type: integer + default: 8080 + backend_max_conn: + description: > + The backend max connectons. + default: 32 + interfaces: + cloudify.interfaces.lifecycle: + create: scripts/haproxy/install-ubuntu.sh + configure: + implementation: scripts/haproxy/haproxy.py + inputs: + invocation: + default: + function: configure + start: + implementation: scripts/haproxy/haproxy.py + inputs: + invocation: + default: + function: start + stop: + implementation: scripts/haproxy/haproxy.py + inputs: + invocation: + default: + function: stop + +relationships: + app_connected_to_haproxy: + derived_from: cloudify.relationships.connected_to + target_interfaces: + cloudify.interfaces.relationship_lifecycle: + establish: + implementation: scripts/haproxy/haproxy.py + inputs: + invocation: + default: + function: add_backend + kwargs: + port: { get_property: [ TARGET, backend_app_port ] } + maxconn: { get_property: [ TARGET, backend_max_conn ] } + # backend_address: (optional, defaults to host_ip) + unlink: + implementation: scripts/haproxy/haproxy.py + inputs: + invocation: + default: + function: remove_backend