├── .busted ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── config.ld ├── haproxy-scm-0.rockspec ├── haproxy.cfg ├── lua ├── haproxy │ ├── apps │ │ ├── config.lua │ │ ├── stats │ │ │ ├── init.lua │ │ │ └── views │ │ │ │ ├── info.lua │ │ │ │ ├── init.lua │ │ │ │ ├── proxy.lua │ │ │ │ ├── server.lua │ │ │ │ ├── serverstate.lua │ │ │ │ └── serverweight.lua │ │ └── uname.lua │ ├── core.lua │ ├── embed │ │ ├── http.lua │ │ ├── jsonify.lua │ │ ├── request.lua │ │ ├── response.lua │ │ └── view.lua │ ├── init.lua │ ├── process.lua │ ├── service.lua │ ├── stats.lua │ └── util.lua └── main.lua ├── rockspecs ├── README.md └── haproxy-scm-0.rockspec ├── spec ├── fixtures │ ├── info.json │ ├── info.txt │ ├── stats.csv │ └── stats.json └── haproxy_spec.lua └── vendor ├── Makefile └── SHA256SUMS /.busted: -------------------------------------------------------------------------------- 1 | -- vim: set ft=lua: 2 | 3 | return { 4 | _all = { 5 | lpath = './lua/?.lua;./lua/?/?.lua;./lua/?/init.lua', 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rock 2 | .vagrant/ 3 | build/ 4 | dist/ 5 | doc/ 6 | luacov.* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ben Webber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all check clean depend dist doc install uninstall test 2 | 3 | PROJECT = lua-haproxy 4 | PACKAGE = haproxy 5 | VERSION := $(shell git describe --always --dirty) 6 | RELEASE = $(PROJECT)-api-$(VERSION) 7 | OS := $(shell uname -s | tr [:upper:] [:lower:]) 8 | ARCH := $(shell uname -m) 9 | BUILD = $(RELEASE)-$(OS)-$(ARCH) 10 | 11 | ROCKSPEC = $(PACKAGE)-scm-0.rockspec 12 | ROCK = $(basename $(ROCKSPEC)).all.rock 13 | 14 | PWD := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 15 | LUA := $(PWD)/vendor/bin/lua 16 | LUAROCKS := $(PWD)/vendor/bin/luarocks 17 | AMALG := $(PWD)/vendor/bin/amalg.lua 18 | 19 | export LUA_PATH := $(shell $(LUAROCKS) path --lr-path);; 20 | export LUA_CPATH := $(shell $(LUAROCKS) path --lr-cpath);; 21 | 22 | all: 23 | mkdir -p build 24 | cp -r lua/* build 25 | cd build && $(LUA) -l amalg main.lua 26 | cd build && $(AMALG) -s main.lua -a -c -x | sed 's|$(PWD)||g' > $(RELEASE).lua 27 | cd build && ln -sf $(RELEASE).lua $(PROJECT)-api-latest.lua 28 | 29 | check: 30 | luacheck lua/ 31 | 32 | clean: 33 | $(RM) $(ROCK) 34 | $(RM) -r build 35 | $(RM) -r dist 36 | 37 | depend: 38 | $(LUAROCKS) install dkjson 39 | $(LUAROCKS) install luafilesystem 40 | $(LUAROCKS) install penlight 41 | $(LUAROCKS) install router 42 | 43 | dist: all $(ROCK) 44 | mkdir -p dist 45 | tar -czf dist/$(BUILD).tar.gz -C build $(RELEASE).lua 46 | mv $(ROCK) dist/$(ROCK) 47 | 48 | $(ROCK): 49 | luarocks make --pack-binary-rock 50 | 51 | install: 52 | luarocks make install 53 | 54 | uninstall: 55 | luarocks remove $(PACKAGE) 56 | 57 | doc: 58 | ldoc . 59 | 60 | test: 61 | busted 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-haproxy 2 | 3 | **lua-haproxy** provides: 4 | 5 | * a high-level Lua binding for the [HAProxy stats interface](https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#9.2) 6 | * an embedded HTTP API that runs as a [service inside HAProxy](http://www.arpalert.org/src/haproxy-lua-api/1.6/) 7 | 8 | ## Requirements 9 | 10 | * Linux 11 | * HAProxy 1.6+ with Lua support 12 | 13 | To check if your version includes Lua, run: 14 | 15 | ``` 16 | haproxy -vv | grep Lua 17 | ``` 18 | * HAProxy must run in single-process mode (default) 19 | * Lua 5.3 and Luarocks for development 20 | 21 | ## Install 22 | 23 | 1. Clone the repo. 24 | 25 | ``` 26 | git clone https://github.com/benwebber/lua-haproxy 27 | ``` 28 | 2. Install the rock. 29 | 30 | ``` 31 | luarocks install haproxy-scm-0.rockspec 32 | ``` 33 | 3. Configure HAProxy to run the Lua service. 34 | 35 | It might be convenient to run the API on a dedicated port: 36 | 37 | ``` 38 | global 39 | stats socket /run/haproxy/admin.sock mode 660 level admin 40 | lua-load /usr/local/bin/lua-haproxy-api 41 | 42 | listen http 43 | bind *:8000 44 | mode http 45 | http-request use-service lua.haproxy-api 46 | ``` 47 | 48 | ## Embedded API 49 | 50 | The [main entry point](lua/main.lua) for this package provides an embedded HTTP API. The API runs as a Lua service inside HAProxy. Use it to introspect the parent process. 51 | 52 | ### Overview 53 | 54 | Requests that create or modify resources should be encoded as JSON (`Content-Type: application/json`). 55 | 56 | The API will return structured data as JSON, and unstructured data as plain text (`Content-Type: text/plain`). 57 | 58 | ### Endpoints 59 | 60 | #### `config` 61 | 62 | | METHOD | PATH | DESCRIPTION | 63 | |--------|-----------|-----------------------------------------------------| 64 | | `GET` | `/config` | Return the active configuration file in plain text. | 65 | 66 | 67 | #### `stats` 68 | 69 | | METHOD | PATH | DESCRIPTION | 70 | |---------|---------------------------------------------------|----------------------------------------------------------------| 71 | | `GET` | `/stats` | Query the stats socket for process statistics. | 72 | | `GET` | `/stats/backends` | Show statistics for all backends. | 73 | | `GET` | `/stats/backends/:backend` | Show statistics for the named backend. | 74 | | `GET` | `/stats/backends/:backend/servers` | Show statistics for all servers in the named backend. | 75 | | `GET` | `/stats/backends/:backend/servers/:server` | Show statistics for the a specific server in the named backed. | 76 | | `GET` | `/stats/backends/:backend/servers/:server/weight` | Get the current weight of a server. | 77 | | `PATCH` | `/stats/backends/:backend/servers/:server/weight` | Set the weight of a server. | 78 | | `GET` | `/stats/backends/:backend/servers/:server/state` | Get the current state of a server. | 79 | | `PATCH` | `/stats/backends/:backend/servers/:server/state` | Set the state of a server. | 80 | | `GET` | `/stats/info` | Query the stats socket for process info. | 81 | | `GET` | `/stats/frontends` | Show statistics for all frontends. | 82 | | `GET` | `/stats/frontends/:frontend` | Show statistics for the named frontend. | 83 | 84 | ## Documentation 85 | 86 | Refer to the Lua API documentation for details: 87 | 88 | https://benwebber.github.io/lua-haproxy/ 89 | 90 | ## Caveats 91 | 92 | **lua-haproxy** is *not* production-ready. Both the internal Lua API and embedded HTTP APIs are unstable. 93 | 94 | ## License 95 | 96 | MIT 97 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | $script = <', 1) 22 | local text = text:gsub('`', '', 1) 23 | local link = 'How Lua runs in HAProxy: ' .. text 24 | local url = api_url:format(anchor) 25 | return link, url 26 | end) 27 | 28 | custom_see_handler('^haproxy (%d%.%d%.%d): (.*)$', function(version, filename) 29 | local haproxy_gitweb = 'http://git.haproxy.org/?p=%s;a=blob;f=%s;hb=v%s' 30 | local repo = 'haproxy-' .. version:match('^(%d%.%d)') .. '.git' 31 | local url = haproxy_gitweb:format(repo, filename, version) 32 | local link = 'HAProxy ' .. version .. ' source: ' .. filename .. '' 33 | return link, url 34 | end) 35 | -------------------------------------------------------------------------------- /haproxy-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | ./rockspecs/haproxy-scm-0.rockspec -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | stats socket /tmp/haproxy.sock mode 660 level admin 3 | stats timeout 30s 4 | lua-load build/lua-haproxy-api-latest.lua 5 | 6 | defaults 7 | log global 8 | mode http 9 | timeout connect 5000 10 | timeout client 50000 11 | timeout server 50000 12 | 13 | listen http 14 | bind *:8000 15 | http-request use-service lua.haproxy-api 16 | -------------------------------------------------------------------------------- /lua/haproxy/apps/config.lua: -------------------------------------------------------------------------------- 1 | local core = require('haproxy.core') 2 | local process = require('haproxy.process') 3 | local http = require('haproxy.embed.http') 4 | local Response = require('haproxy.embed.response') 5 | local View = require('haproxy.embed.view') 6 | 7 | local ConfigView = View.new('ConfigView') 8 | 9 | function ConfigView:get(request, context) 10 | return Response(http.status.OK, core.ctx.config, { ['Content-Type'] = 'text/plain', }) 11 | end 12 | 13 | local function init() 14 | core.ctx.config = process.config() 15 | end 16 | 17 | return { 18 | init = init, 19 | routes = { 20 | ['/'] = ConfigView.as_view, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/init.lua: -------------------------------------------------------------------------------- 1 | local stats = require('haproxy.stats') 2 | local View = require('haproxy.embed.view') 3 | local jsonify = require('haproxy.embed.jsonify') 4 | local process = require('haproxy.process') 5 | 6 | local views = require('haproxy.apps.stats.views') 7 | 8 | local StatsView = View.new('StatsView') 9 | 10 | local function init() 11 | core.ctx.stats = stats.Client(process.stats_socket()) 12 | end 13 | 14 | function StatsView:get(request, context) 15 | return jsonify(context.stats:stats()) 16 | end 17 | 18 | return { 19 | init = init, 20 | routes = { 21 | ['/'] = StatsView.as_view, 22 | ['/info'] = views.InfoView.as_view, 23 | ['/backends'] = views.ProxyView.as_view, 24 | ['/backends/:backend'] = views.ProxyView.as_view, 25 | ['/backends/:backend/servers'] = views.ServerView.as_view, 26 | ['/backends/:backend/servers/:server'] = views.ServerView.as_view, 27 | ['/backends/:backend/servers/:server/weight'] = views.ServerWeightView.as_view, 28 | ['/backends/:backend/servers/:server/state'] = views.ServerStateView.as_view, 29 | ['/frontends'] = views.ProxyView.as_view, 30 | ['/frontends/:frontend'] = views.ProxyView.as_view, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/info.lua: -------------------------------------------------------------------------------- 1 | local View = require('haproxy.embed.view') 2 | local jsonify = require('haproxy.embed.jsonify') 3 | 4 | local InfoView = View.new('InfoView') 5 | 6 | function InfoView:get(request, context) 7 | return jsonify(context.stats:info()) 8 | end 9 | 10 | return InfoView 11 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | InfoView = require('haproxy.apps.stats.views.info'), 3 | ProxyView = require('haproxy.apps.stats.views.proxy'), 4 | ServerStateView = require('haproxy.apps.stats.views.serverstate'), 5 | ServerView = require('haproxy.apps.stats.views.server'), 6 | ServerWeightView = require('haproxy.apps.stats.views.serverweight'), 7 | } 8 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/proxy.lua: -------------------------------------------------------------------------------- 1 | local View = require('haproxy.embed.view') 2 | local jsonify = require('haproxy.embed.jsonify') 3 | 4 | local ProxyView = View.new('ProxyView') 5 | 6 | function ProxyView:get(request, context) 7 | if request.path:find("/backends") then 8 | if request.view_args.backend then 9 | return jsonify(context.stats:get_backend(request.view_args.backend)) 10 | end 11 | return jsonify(context.stats:get_backends()) 12 | end 13 | 14 | if request.view_args.frontend then 15 | return jsonify(context.stats:get_frontend(request.view_args.frontend)) 16 | end 17 | return jsonify(context.stats:get_frontends()) 18 | end 19 | 20 | return ProxyView 21 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/server.lua: -------------------------------------------------------------------------------- 1 | local http = require('haproxy.embed.http') 2 | local jsonify = require('haproxy.embed.jsonify') 3 | local Response = require('haproxy.embed.response') 4 | local View = require('haproxy.embed.view') 5 | 6 | local ServerView = View.new('ServerView') 7 | 8 | function ServerView:get(request, context) 9 | if request.view_args.server then 10 | return jsonify(context.stats:get_server(request.view_args.backend, request.view_args.server)) 11 | end 12 | return jsonify(context.stats:get_servers(request.view_args.backend)) 13 | end 14 | 15 | function ServerView:patch(request, context) 16 | local data = request:json() 17 | local backend = context.stats:get_backend(request.view_args.backend) 18 | if data.status == ('DOWN' or 'MAINT') then 19 | context.stats:disable_server(backend.pxname, request.view_args.server) 20 | elseif data.status == 'UP' then 21 | context.stats:enable_server(backend.pxname, request.view_args.server) 22 | else 23 | return Response(http.status.BAD_REQUEST) 24 | end 25 | return Response(http.status.OK) 26 | end 27 | 28 | return ServerView 29 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/serverstate.lua: -------------------------------------------------------------------------------- 1 | local http = require('haproxy.embed.http') 2 | local jsonify = require('haproxy.embed.jsonify') 3 | local Response = require('haproxy.embed.response') 4 | local View = require('haproxy.embed.view') 5 | 6 | local ServerStateView = View.new('ServerStateView') 7 | 8 | function ServerStateView:get(request, context) 9 | return jsonify(context.stats:get_state(request.view_args.backend, request.view_args.server)) 10 | end 11 | 12 | function ServerStateView:patch(request, context) 13 | local data = request:json() 14 | context.stats:set_weight(request.view_args.backend, request.view_args.server, data.current) 15 | return Response(http.status.OK) 16 | end 17 | 18 | return ServerStateView 19 | -------------------------------------------------------------------------------- /lua/haproxy/apps/stats/views/serverweight.lua: -------------------------------------------------------------------------------- 1 | local http = require('haproxy.embed.http') 2 | local jsonify = require('haproxy.embed.jsonify') 3 | local Response = require('haproxy.embed.response') 4 | local View = require('haproxy.embed.view') 5 | 6 | local ServerWeightView = View.new('ServerWeightView') 7 | 8 | function ServerWeightView:get(request, context) 9 | return jsonify(context.stats:get_weight(request.view_args.backend, request.view_args.server)) 10 | end 11 | 12 | function ServerWeightView:patch(request, context) 13 | local data = request:json() 14 | context.stats:set_weight(request.view_args.backend, request.view_args.server, data.current) 15 | return Response(http.status.OK) 16 | end 17 | 18 | return ServerWeightView 19 | -------------------------------------------------------------------------------- /lua/haproxy/apps/uname.lua: -------------------------------------------------------------------------------- 1 | local core = require('haproxy.core') 2 | local util = require('haproxy.util') 3 | 4 | local http = require('haproxy.embed.http') 5 | local Response = require('haproxy.embed.response') 6 | local View = require('haproxy.embed.view') 7 | 8 | local UnameView = View.new('UnameView') 9 | 10 | function UnameView:get(request, context) 11 | return Response(http.status.OK, core.ctx.uname, { ['Content-Type'] = 'text/plain', }) 12 | end 13 | 14 | local function init() 15 | core.ctx.uname = util.uname() 16 | end 17 | 18 | return { 19 | init = init, 20 | routes = { 21 | ['/'] = UnameView.as_view, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lua/haproxy/core.lua: -------------------------------------------------------------------------------- 1 | --- Extends HAProxy core API. 2 | --- @module haproxy.core 3 | local core = _G['core'] or {} 4 | 5 | --- Use `core.ctx` to share state between apps. 6 | core.ctx = {} 7 | 8 | -- Problem: amalg executes a script (`lua -l amalg`) to identify its 9 | -- dependencies However, HAProxy API functions are only available in an HAProxy 10 | -- execution context. 11 | -- 12 | -- Solution: stub a `core` table to satisfy execution at build time. The table's 13 | -- metatable will provide anonymous functions to satisfy calls like 14 | -- `core.register_service()`. 15 | if package.loaded['amalg'] then 16 | setmetatable(core, { __index = function() return function() return end end }) 17 | end 18 | 19 | return core 20 | -------------------------------------------------------------------------------- /lua/haproxy/embed/http.lua: -------------------------------------------------------------------------------- 1 | --- Embedded HTTP framework. 2 | -- @module haproxy.embed 3 | 4 | local http = {} 5 | 6 | --- RFC2616 HTTP/1.1 status codes 7 | http.status = { 8 | CONTINUE = 100, -- `100 Continue` 9 | SWITCHING_PROTOCOLS = 101, -- `101 Switching Protocols` 10 | OK = 200, -- `200 OK` 11 | CREATED = 201, -- `201 Created` 12 | ACCEPTED = 202, -- `202 Accepted` 13 | NON_AUTHORITATIVE_INFORMATION = 203, -- `203 Non-Authoritative Information` 14 | NO_CONTENT = 204, -- `204 No Content` 15 | RESET_CONTENT = 205, -- `205 Reset Content` 16 | PARTIAL_CONTENT = 206, -- `206 Partial Content` 17 | MULTIPLE_CHOICES = 300, -- `300 Multiple Choices` 18 | MOVED_PERMANENTLY = 301, -- `301 Moved Permanently` 19 | FOUND = 302, -- `302 Found` 20 | SEE_OTHER = 303, -- `303 See Other` 21 | NOT_MODIFIED = 304, -- `304 Not Modified` 22 | USE_PROXY = 305, -- `305 Use Proxy` 23 | TEMPORARY_REDIRECT = 307, -- `307 Temporary Redirect` 24 | BAD_REQUEST = 400, -- `400 Bad Request` 25 | UNAUTHORIZED = 401, -- `401 Unauthorized` 26 | PAYMENT_REQUIRED = 402, -- `402 Payment Required` 27 | FORBIDDEN = 403, -- `403 Forbidden` 28 | NOT_FOUND = 404, -- `404 Not Found` 29 | METHOD_NOT_ALLOWED = 405, -- `405 Method Not Allowed` 30 | NOT_ACCEPTABLE = 406, -- `406 Not Acceptable` 31 | PROXY_AUTHENTICATION_REQUIRED = 407, -- `407 Proxy Authentication Required` 32 | REQUEST_TIME_OUT = 408, -- `408 Request Time-out` 33 | CONFLICT = 409, -- `409 Conflict` 34 | GONE = 410, -- `410 Gone` 35 | LENGTH_REQUIRED = 411, -- `411 Length Required` 36 | PRECONDITION_FAILED = 412, -- `412 Precondition Failed` 37 | REQUEST_ENTITY_TOO_LARGE = 413, -- `413 Request Entity Too Large` 38 | REQUEST_URI_TOO_LARGE = 414, -- `414 Request-URI Too Large` 39 | UNSUPPORTED_MEDIA_TYPE = 415, -- `415 Unsupported Media Type` 40 | REQUESTED_RANGE_NOT_SATISFIABLE = 416, -- `416 Requested range not satisfiable` 41 | EXPECTATION_FAILED = 417, -- `417 Expectation Failed` 42 | IM_A_TEAPOT = 418, -- `I'm a teapot` 43 | INTERNAL_SERVER_ERROR = 500, -- `500 Internal Server Error` 44 | NOT_IMPLEMENTED = 501, -- `501 Not Implemented` 45 | BAD_GATEWAY = 502, -- `502 Bad Gateway` 46 | SERVICE_UNAVAILABLE = 503, -- `503 Service Unavailable` 47 | GATEWAY_TIME_OUT = 504, -- `504 Gateway Time-out` 48 | HTTP_VERSION_NOT_SUPPORTED = 505, -- `505 HTTP Version not supported` 49 | } 50 | 51 | --- RFC2616 HTTP/1.1 methods 52 | http.method = { 53 | DELETE = 'DELETE', 54 | GET = 'GET', 55 | HEAD = 'HEAD', 56 | OPTIONS = 'OPTIONS', 57 | PATCH = 'PATCH', 58 | POST = 'POST', 59 | PUT = 'PUT', 60 | } 61 | 62 | return http 63 | -------------------------------------------------------------------------------- /lua/haproxy/embed/jsonify.lua: -------------------------------------------------------------------------------- 1 | --- Embedded HTTP server. 2 | -- @module haproxy.embed 3 | 4 | local json = require('dkjson') 5 | 6 | local http = require('haproxy.embed.http') 7 | local Response = require('haproxy.embed.response') 8 | 9 | --- Check if `value` is a table. 10 | -- @param value object 11 | -- @treturn bool `true` or `false` 12 | local function is_table(value) return type(value) == 'table' end 13 | 14 | --- Build a JSON response. 15 | -- Sets `Content-Type: application/json` and `Content-Length` based on the 16 | -- length of the serialized obj. If `obj` is a table, sorts the keys. 17 | -- @param obj object to serialize 18 | -- @treturn Response the JSON response 19 | -- @function jsonify 20 | local function jsonify(obj) 21 | local json_encode_state = {} 22 | if is_table(obj) then 23 | local keys = {} 24 | local n = 0 25 | for k, _ in pairs(obj) do 26 | n = n + 1 27 | keys[n] = k 28 | end 29 | table.sort(keys) 30 | json_encode_state = { keyorder = keys } 31 | end 32 | local body = json.encode(obj, json_encode_state) 33 | local headers = { 34 | ['Content-Type'] = 'application/json', 35 | } 36 | return Response(http.status.OK, body, headers) 37 | end 38 | 39 | -- @export 40 | return jsonify 41 | -------------------------------------------------------------------------------- /lua/haproxy/embed/request.lua: -------------------------------------------------------------------------------- 1 | --- HTTP request 2 | -- @classmod haproxy.embed.Request 3 | -- @pragma nostrip 4 | 5 | local json = require('dkjson') 6 | 7 | local Set = require('pl.set') 8 | local class = require('pl.class') 9 | local stringx = require('pl.stringx') 10 | local tablex = require('pl.tablex') 11 | local url = require('pl.url') 12 | 13 | local http = require('haproxy.embed.http') 14 | 15 | --- A Request represents an API request. 16 | local Request = class() 17 | 18 | function Request:_init(request) 19 | local fields = Set{ 20 | 'method', 21 | 'path', 22 | 'data', 23 | 'headers', 24 | 'args', 25 | 'form', 26 | 'view_args', 27 | } 28 | for k, v in pairs(request) do 29 | if fields[k] then 30 | self[k] = v 31 | end 32 | end 33 | end 34 | 35 | --- Parse request query string. 36 | -- @tparam string qs query string 37 | -- @treturn table parsed results 38 | function Request.parse_query_string(qs) 39 | local result = {} 40 | if not qs then 41 | return result 42 | end 43 | local args = stringx.split(url.unquote(qs), '&') 44 | for _, pair in pairs(args) do 45 | local k, _, v = stringx.partition(pair, '=') 46 | result[k] = v 47 | end 48 | return result 49 | end 50 | 51 | --- Construct a Request from an AppletHTTP instance. 52 | -- @tparam AppletHTTP applet 53 | -- @treturn Request 54 | function Request.from_applet(applet) 55 | local unsafe_methods = Set{http.method.PATCH, http.method.POST, http.method.PUT} 56 | local request = Request{ 57 | method = applet.method, 58 | path = applet.path, 59 | headers = applet.headers, 60 | args = Request.parse_query_string(applet.qs), 61 | } 62 | if unsafe_methods[applet.method] then 63 | request.data = applet:receive() 64 | end 65 | return request 66 | end 67 | 68 | --- Decode JSON data in request body. 69 | -- @tparam ?bool force Decode request data as JSON regardless of Content-Type. 70 | -- @treturn table decoded data 71 | function Request:json(force) 72 | -- Because headers can have multiple values, HAProxy provides values as a 73 | -- 0-indexed array. 74 | if tablex.find(self.headers['content-type'], 'application/json', 0) or force then 75 | return json.decode(self.data) 76 | end 77 | return nil 78 | end 79 | 80 | Request.new = Request 81 | 82 | return Request 83 | -------------------------------------------------------------------------------- /lua/haproxy/embed/response.lua: -------------------------------------------------------------------------------- 1 | --- HTTP response 2 | -- @classmod haproxy.embed.Response 3 | -- @pragma nostrip 4 | 5 | local class = require('pl.class') 6 | local tablex = require('pl.tablex') 7 | 8 | local http = require('haproxy.embed.http') 9 | 10 | --- A Response represents an API response. 11 | local Response = class() 12 | 13 | --- Construct an API response. 14 | -- @tparam[opt=200] int status_code HTTP status code 15 | -- @tparam[opt=''] string body response body 16 | -- @tparam[opt={}] table headers response headers 17 | -- @usage response = Response(http.status.OK, 'Hello world!', {}) 18 | -- @function Response.new 19 | -- @see http.status 20 | function Response:_init(status_code, body, headers) 21 | self.status_code = status_code or http.status.OK 22 | self.body = body or '' 23 | local default_headers = { ['Content-Length'] = self.body:len() } 24 | self.headers = tablex.update(default_headers, headers or {}) 25 | end 26 | 27 | Response.new = Response 28 | 29 | --- Render and serve an API response. 30 | -- @param applet the HAProxy applet context 31 | function Response:render(applet) 32 | applet:set_status(self.status_code) 33 | for header, value in pairs(self.headers) do 34 | applet:add_header(header, value) 35 | end 36 | applet:start_response() 37 | applet:send(self.body) 38 | end 39 | 40 | return Response 41 | -------------------------------------------------------------------------------- /lua/haproxy/embed/view.lua: -------------------------------------------------------------------------------- 1 | --- Simple class-based views. 2 | -- @classmod haproxy.embed.View 3 | 4 | local class = require('pl.class') 5 | local stringx = require('pl.stringx') 6 | 7 | local Response = require('haproxy.embed.response') 8 | local http = require('haproxy.embed.http') 9 | 10 | local View = class() 11 | 12 | --- Dispatch a request to the method handler. 13 | -- @tparam haproxy.embed.Request request 14 | -- @tparam table context request context (i.e., shared state) 15 | -- @treturn haproxy.embed.Response 16 | -- @see haproxy.core.ctx 17 | function View:dispatch(request, context) 18 | local method = request.method:lower() 19 | if self[method] then 20 | return self[method](self, request, context) 21 | end 22 | local methods = stringx.join(', ', self:methods()) 23 | return Response(http.status.METHOD_NOT_ALLOWED, '', { Allow = methods }) 24 | end 25 | 26 | --- Default handler for OPTIONS requests. 27 | -- Returns HTTP 204 No Content and a list of allowed methods. 28 | -- @tparam haproxy.embed.Request request 29 | -- @treturn haproxy.embed.Response 30 | function View:options(request) 31 | local methods = stringx.join(', ', self:methods()) 32 | return Response(http.status.NO_CONTENT, '', { 33 | Allow = methods, 34 | }) 35 | end 36 | 37 | --- Create a new named View (i.e., subclass of View). 38 | -- @tparam string name 39 | -- @treturn haproxy.embed.View 40 | function View.new(name) 41 | local cls = class(View) 42 | cls._name = name 43 | cls.as_view = function(request, context) return cls():dispatch(request, context) end 44 | return cls 45 | end 46 | 47 | --- Initialize and immediately dispatch the view. 48 | -- Useful for assigning a class-based view to a route. 49 | -- 50 | -- **NOTE:** This is a class method, not an instance method. It does not take an 51 | -- implicit self argument. 52 | -- @function as_view 53 | -- @tparam haproxy.embed.Request request 54 | -- @tparam table context request context (i.e., shared state) 55 | -- @treturn haproxy.embed.Response 56 | -- @see haproxy.core.ctx 57 | -- @todo Customize LDoc to mark this as a class method. 58 | -- @usage response = MyView.as_view(request, context) 59 | 60 | --- Return list of methods handled by the view. 61 | -- @treturn table list of available methods 62 | function View:methods() 63 | local methods = {} 64 | for _, method in pairs(http.method) do 65 | if self[method:lower()] then 66 | methods[#methods + 1] = method 67 | end 68 | end 69 | table.sort(methods) 70 | return methods 71 | end 72 | 73 | return View 74 | -------------------------------------------------------------------------------- /lua/haproxy/init.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benwebber/lua-haproxy/214ea4a8f0df28079e23c0241d2b7988e3c85841/lua/haproxy/init.lua -------------------------------------------------------------------------------- /lua/haproxy/process.lua: -------------------------------------------------------------------------------- 1 | --- Inspect the parent HAProxy process. 2 | --- @module haproxy.process 3 | local M = {} 4 | 5 | --- Return the command line used to invoke HAProxy. 6 | -- @treturn string 7 | function M.cmdline() 8 | local f = assert(io.open('/proc/self/cmdline', 'rb')) 9 | local s = f:read('*a') 10 | f:close() 11 | return s 12 | end 13 | 14 | --- Return the current loaded configuration. Assembles all configuration files 15 | -- passed on the command line. 16 | -- @treturn string 17 | function M.config() 18 | local cmdline = M.cmdline() 19 | local files = {} 20 | local previous 21 | local remainder = false 22 | for token in string.gmatch(cmdline, '%g+') do 23 | -- -- signifies that the remaining arguments are configuration files. 24 | if token == '--' then 25 | remainder = true 26 | elseif previous == '-f' or remainder then 27 | files[#files+1] = token 28 | end 29 | previous = token 30 | end 31 | local config = '' 32 | for _, file in ipairs(files) do 33 | local f = assert(io.open(file, 'r')) 34 | config = config .. f:read('*a') 35 | f:close() 36 | end 37 | return config 38 | end 39 | 40 | --- Attempt to locate the process's stats socket. 41 | -- @treturn string path to socket 42 | function M.stats_socket() 43 | local config = M.config() 44 | return string.match(config, 'stats%s+socket%s+(%S+)') 45 | end 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /lua/haproxy/service.lua: -------------------------------------------------------------------------------- 1 | --- Service wraps the service configuration and HTTP router. 2 | -- @classmod haproxy.Service 3 | -- @pragma nostrip 4 | 5 | local router = require('router') 6 | 7 | local class = require('pl.class') 8 | 9 | local Request = require('haproxy.embed.request') 10 | local Response = require('haproxy.embed.response') 11 | local http = require('haproxy.embed.http') 12 | local core = require('haproxy.core') 13 | local util = require('haproxy.util') 14 | 15 | local Service = class() 16 | 17 | --- Initialize a new API instance. 18 | -- @tparam table config application configuration 19 | -- @function Service.new 20 | function Service:_init() 21 | self.router = router.new() 22 | end 23 | 24 | Service.new = Service 25 | 26 | --- Dispatch a request to the router and return the response. 27 | -- @param request HTTP request 28 | -- @treturn Response the API response 29 | function Service:dispatch(request) 30 | local view, view_args = self.router:resolve(request.method, request.path) 31 | -- unknown method 32 | if view == nil then 33 | return Response(http.status.NOT_IMPLEMENTED) 34 | -- unknown route 35 | elseif not view then 36 | return Response(http.status.NOT_FOUND) 37 | end 38 | request.view_args = view_args 39 | return view(request, core.ctx) 40 | end 41 | 42 | --- Register routes. 43 | -- @tparam table routes routes to register 44 | -- @tparam ?string prefix prepend routes with given prefix 45 | -- @usage Service:register_routes({ ['/info'] = api_info }, '/api') 46 | function Service:register_routes(routes, prefix) 47 | local prefix = prefix or '' 48 | -- We defer method handling to views. Configure the router to send all methods 49 | -- to each view. 50 | local matches = {} 51 | for _, method in pairs(http.method) do 52 | matches[method] = {} 53 | for url, view in pairs(routes) do 54 | matches[method][prefix .. url] = view 55 | end 56 | end 57 | self.router:match(matches) 58 | end 59 | 60 | --- Mount an application. 61 | -- Mounting an application adds its routes to the top-level router. 62 | -- @param app application module 63 | -- @tparam ?string prefix Prepend routes with this prefix. 64 | function Service:mount(app, prefix) 65 | local prefix = prefix or '' 66 | 67 | if util.has_function(app, 'init') then app.init() end 68 | 69 | local prefixed_routes = {} 70 | for route, view in pairs(app.routes) do 71 | prefixed_routes[prefix .. route] = view 72 | end 73 | self:register_routes(prefixed_routes) 74 | end 75 | 76 | --- Serve a request. 77 | -- @param applet HAProxy AppletHTTP instance 78 | function Service:serve(applet) 79 | local request = Request.from_applet(applet) 80 | local response = self:dispatch(request) 81 | response:render(applet) 82 | end 83 | 84 | return Service 85 | -------------------------------------------------------------------------------- /lua/haproxy/stats.lua: -------------------------------------------------------------------------------- 1 | --- Stats socket client. 2 | -- @module haproxy.stats 3 | local class = require('pl.class') 4 | local stringx = require('pl.stringx') 5 | local tablex = require('pl.tablex') 6 | 7 | local core = require('haproxy.core') 8 | local util = require('haproxy.util') 9 | 10 | --- HAProxy stats types. 11 | -- These are integer codes used by the stats interface (specifically `show 12 | -- stat`) to represent the source of each datum. 13 | -- @see haproxy 1.6.3: include/proto/dumpstats.h 14 | local stats_types = { 15 | FRONTEND = 0, -- frontend 16 | BACKEND = 1, -- backend 17 | SERVER = 2, -- server 18 | SOCKET = 3, -- socket 19 | } 20 | 21 | --- Parse HAProxy info output to a table. 22 | -- The JSON representation of this table would be a sorted hash. 23 | -- @tparam string data key-value data 24 | -- @tparam[opt=':'] string sep key-value separator 25 | -- @treturn table parsed data 26 | local function parse_info(data, sep) 27 | local sep = sep or ':' 28 | local result = {} 29 | data = stringx.strip(data) 30 | for line in stringx.lines(data) do 31 | local k, _, v = stringx.partition(line, sep) 32 | v = stringx.strip(v) 33 | if tonumber(v) then 34 | v = tonumber(v) 35 | end 36 | result[k] = v 37 | end 38 | return result 39 | end 40 | 41 | --- Convert HAProxy stats CSV output to a table. 42 | -- The JSON representation of this table would be an array of hashes. 43 | -- @tparam string csv CSV data 44 | -- @tparam[opt=','] string sep CSV separator 45 | -- @treturn table parsed data 46 | local function parse_stats(csv, sep) 47 | local results = {} 48 | local headers, _, data = stringx.partition(csv, '\n') 49 | local sep = sep or ',' 50 | data = stringx.strip(data) 51 | headers = stringx.lstrip(headers, '# ') 52 | headers = stringx.rstrip(headers, sep) 53 | headers = stringx.split(headers, sep) 54 | for line in stringx.lines(data) do 55 | local result = {} 56 | local fields = stringx.rstrip(line, sep) 57 | fields = stringx.split(fields, sep) 58 | for i, field in ipairs(fields) do 59 | if tonumber(field) then 60 | field = tonumber(field) 61 | end 62 | result[headers[i]] = field 63 | end 64 | results[#results+1] = result 65 | end 66 | return results 67 | end 68 | 69 | --- HAProxy stats socket client 70 | -- @type Client 71 | local Client = class() 72 | 73 | --- Create a new HAProxy client. 74 | -- @tparam string address stats interface address 75 | -- @tparam[opt] int stats interface port 76 | -- @tparam[opt] int stats connection timeout in seconds 77 | -- @function Client.new 78 | function Client:_init(address, port, timeout) 79 | self.address = address or '/run/haproxy/admin.sock' 80 | self.port = port or nil 81 | self.timeout = timeout or 3 82 | end 83 | 84 | Client.new = Client 85 | 86 | --- Create a new TCP `Socket` instance. If running inside HAProxy, use the 87 | -- LuaSocket-compatible `Socket` class. Otherwise, use LuaSocket. 88 | -- @treturn Socket 89 | -- @see haproxy_lua: `Socket` 90 | -- @see luarock: LuaSocket 91 | function Client.tcp() 92 | if util.has_function(core, 'tcp') then 93 | -- running inside HAProxy 94 | return core.tcp() 95 | end 96 | -- running outside HAProxy 97 | local socket = require('socket') 98 | return socket.tcp() 99 | end 100 | 101 | --- Execute an HAProxy stats command. Used by higher-level methods such as 102 | -- `Client:stats`. 103 | -- @tparam string command command to execute 104 | -- @treturn string string response 105 | function Client:execute(command) 106 | local socket = self.tcp() 107 | if self.port then 108 | socket:connect(self.address, self.port) 109 | else 110 | socket:connect(self.address) 111 | end 112 | socket:settimeout(self.timeout) 113 | socket:send(command .. '\n') 114 | local response = socket:receive('*a') 115 | socket:close() 116 | return response 117 | end 118 | 119 | --- Returns table of parsed stats results. 120 | -- @see Client:execute 121 | -- @treturn table parsed stats results 122 | function Client:stats() 123 | return parse_stats(self:execute('show stat')) 124 | end 125 | 126 | --- Returns table of parsed HAProxy instance information. 127 | -- @see Client:execute 128 | -- @treturn table parsed HAProxy instance info 129 | function Client:info() 130 | return parse_info(self:execute('show info')) 131 | end 132 | 133 | --- Place `backend/server` in maintenace. 134 | -- @tparam string backend backend name 135 | -- @tparam string server server name 136 | function Client:disable_server(backend, server) 137 | return self:execute('disable server ' .. backend .. '/' .. server) 138 | end 139 | 140 | --- Place `backend/server` in traffic. 141 | -- @tparam string backend backend name 142 | -- @tparam string server server name 143 | function Client:enable_server(backend, server) 144 | return self:execute('enable server ' .. backend .. '/' .. server) 145 | end 146 | 147 | --- Filter stats for all proxies. 148 | -- @tparam int type_ stats type 149 | -- @treturn table 150 | -- @see stats_types 151 | -- @local 152 | function Client:get_proxies(type_) 153 | return tablex.filter(self:stats(), 154 | function(proxy) return proxy.type == type_ end) 155 | end 156 | 157 | --- Return a specific proxy. 158 | -- @tparam int type_ stats type 159 | -- @tparam string id_or_name ID or name 160 | -- @treturn table 161 | -- @local 162 | function Client:get_proxy(type_, id_or_name) 163 | local proxies = self:get_proxies(type_) 164 | 165 | if not id_or_name then 166 | return proxies 167 | end 168 | 169 | if tonumber(id_or_name) then 170 | return tablex.filter(proxies, function(proxy) return proxy.iid == tonumber(id_or_name) end)[1] 171 | else 172 | return tablex.filter(proxies, function(proxy) return proxy.pxname == id_or_name end)[1] 173 | end 174 | end 175 | 176 | --- List all backends. 177 | -- @treturn table 178 | function Client:get_backends() 179 | return self:get_proxies(stats_types.BACKEND) 180 | end 181 | 182 | --- List all frontends. 183 | -- @treturn table 184 | function Client:get_frontends() 185 | return self:get_proxies(stats_types.FRONTEND) 186 | end 187 | 188 | --- Show a specific backend. 189 | -- @param id_or_name backend ID or name 190 | -- @treturn table 191 | function Client:get_backend(id_or_name) 192 | return self:get_proxy(stats_types.BACKEND, id_or_name) 193 | end 194 | 195 | --- Retrieve a specific frontend. 196 | -- @param id_or_name frontend ID or name 197 | -- @treturn table 198 | function Client:get_frontend(id_or_name) 199 | return self:get_proxy(stats_types.FRONTEND, id_or_name) 200 | end 201 | 202 | --- Retrieve a specific server. 203 | -- @tparam string backend backend name 204 | -- @param id_or_name integer server ID or string name 205 | -- @treturn table 206 | function Client:get_server(backend, id_or_name) 207 | local stats = self:stats() 208 | local backend = self:get_backend(backend) 209 | if tonumber(id_or_name) then 210 | return tablex.filter(stats, function(i) return i.iid == backend.iid and i.type == stats_types.SERVER and i.sid == tonumber(id_or_name) end)[1] 211 | else 212 | return tablex.filter(stats, function(i) return i.iid == backend.iid and i.type == stats_types.SERVER and i.svname == id_or_name end)[1] 213 | end 214 | end 215 | 216 | --- Retrieve all servers belonging to a backend. 217 | -- @tparam string backend 218 | -- @treturn table 219 | function Client:get_servers(backend) 220 | local backend = self:get_backend(backend) 221 | local servers = tablex.filter( 222 | self:stats(), 223 | function(i) return i.iid == backend.iid and i.type == stats_types.SERVER end 224 | ) 225 | return servers 226 | end 227 | 228 | --- Retrieve the current and initial server weights. 229 | -- @tparam string backend 230 | -- @tparam string server 231 | -- @treturn table 232 | function Client:get_weight(backend, server) 233 | local response = self:execute('get weight ' .. backend .. '/' .. server) 234 | local current, initial = response:match('(%d+) %(initial (%d+)%)') 235 | return tablex.map(tonumber, { current = current, initial = initial }) 236 | end 237 | 238 | --- Set a server's weight. 239 | -- @tparam string backend 240 | -- @tparam string server 241 | -- @param weight integer weight (`1`) or string percentage (`100%`) 242 | function Client:set_weight(backend, server, weight) 243 | self:execute('set weight ' .. backend .. '/' .. server .. ' ' .. weight) 244 | end 245 | 246 | --- Retrieve a server's current state. 247 | -- @tparam string backend 248 | -- @tparam string server 249 | -- @treturn table 250 | -- @fixme not implemented 251 | function Client:get_state(backend, server) 252 | local response = self:execute('show servers state ' .. backend) 253 | local _, _, data = stringx.partition(response, '\n') 254 | return parse_stats(data, ' ') 255 | end 256 | 257 | -- @section end 258 | 259 | -------------------------------------------------------------------------------- 260 | -- public interface 261 | -------------------------------------------------------------------------------- 262 | 263 | --- @export 264 | return { 265 | Client = Client, 266 | stats_types = stats_types, 267 | parse_info = parse_info, 268 | parse_stats = parse_stats, 269 | } 270 | -------------------------------------------------------------------------------- /lua/haproxy/util.lua: -------------------------------------------------------------------------------- 1 | --- Utilities used by other modules. 2 | --- @module haproxy.util 3 | local M = {} 4 | 5 | --- Calls uname to return the current operating system. 6 | -- @treturn string 7 | function M.uname() 8 | local f = assert(io.popen('uname -s', 'r')) 9 | local os_name = assert(f:read('*a')) 10 | f:close() 11 | return os_name 12 | end 13 | 14 | --- Return whether a table contains the named function. 15 | -- @tparam table obj to inspect 16 | -- @tparam string method function name to check 17 | -- @treturn boolean 18 | function M.has_function(obj, method) 19 | return type(obj) == 'table' and obj[method] and type(obj[method]) == 'function' 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lua/main.lua: -------------------------------------------------------------------------------- 1 | --- Somewhat RESTful API for HAProxy. 2 | -- @script lua-haproxy-api 3 | -- @usage 4 | -- global 5 | -- stats socket /run/haproxy/admin.sock mode 660 level admin 6 | -- lua-load /etc/haproxy/lua-haproxy-api.lua 7 | -- 8 | -- frontend example 9 | -- http-request use-service lua.haproxy-api 10 | local core = require('haproxy.core') 11 | 12 | local Service = require('haproxy.service') 13 | local config = require('haproxy.apps.config') 14 | local stats = require('haproxy.apps.stats') 15 | local uname = require('haproxy.apps.uname') 16 | 17 | -- declared here to satisfy strict mode 18 | local service 19 | 20 | --- Initialize the service. 21 | -- Load the API configuration and routing table. 22 | -- HAProxy executes this function once on startup. 23 | -- @usage core.register_init(init) 24 | local function init() 25 | service = Service() 26 | service:mount(config, '/config') 27 | service:mount(stats, '/stats') 28 | service:mount(uname, '/uname') 29 | end 30 | 31 | --- The main service entry point. 32 | -- @param applet HAProxy applet context 33 | -- @usage core.register_service('haproxy-api', 'http', main) 34 | local function main(applet) 35 | service:serve(applet) 36 | end 37 | 38 | core.register_init(init) 39 | core.register_service('haproxy-api', 'http', main) 40 | -------------------------------------------------------------------------------- /rockspecs/README.md: -------------------------------------------------------------------------------- 1 | # lua-haproxy rockspecs 2 | 3 | **N.B.** In general, directories under the top-level should be singular 4 | (`rockspec` cf. `rockspecs`). However, `luarocks make` does not differentiate 5 | between files and directories matching `rockspec$`. This directory should be 6 | renamed when [the fix][1] makes it to the next LuaRocks release. 7 | 8 | [1]: https://github.com/keplerproject/luarocks/commit/3766d49926771f754bcd255c9e82102cbbe6ce01 9 | -------------------------------------------------------------------------------- /rockspecs/haproxy-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = 'haproxy' 2 | version = 'scm-0' 3 | source = { 4 | url = 'https://github.com/benwebber/lua-haproxy/', 5 | } 6 | description = { 7 | summary = 'High-level Lua client for HAProxy', 8 | homepage = 'https://github.com/benwebber/lua-haproxy/', 9 | license = 'MIT', 10 | } 11 | dependencies = { 12 | 'dkjson', 13 | 'router', 14 | 'lua >= 5.3', 15 | 'penlight >= 1.3', 16 | } 17 | build = { 18 | type = 'builtin', 19 | modules = { 20 | ['haproxy'] = 'lua/haproxy/init.lua', 21 | ['haproxy.apps.config'] = 'lua/haproxy/apps/config.lua', 22 | ['haproxy.apps.stats'] = 'lua/haproxy/apps/stats/init.lua', 23 | ['haproxy.apps.stats.views'] = 'lua/haproxy/apps/stats/views/init.lua', 24 | ['haproxy.apps.stats.views.info'] = 'lua/haproxy/apps/stats/views/info.lua', 25 | ['haproxy.apps.stats.views.proxy'] = 'lua/haproxy/apps/stats/views/proxy.lua', 26 | ['haproxy.apps.stats.views.server'] = 'lua/haproxy/apps/stats/views/server.lua', 27 | ['haproxy.apps.stats.views.serverstate'] = 'lua/haproxy/apps/stats/views/serverstate.lua', 28 | ['haproxy.apps.stats.views.serverweight'] = 'lua/haproxy/apps/stats/views/serverweight.lua', 29 | ['haproxy.core'] = 'lua/haproxy/core.lua', 30 | ['haproxy.embed.http'] = 'lua/haproxy/embed/http.lua', 31 | ['haproxy.embed.jsonify'] = 'lua/haproxy/embed/jsonify.lua', 32 | ['haproxy.embed.request'] = 'lua/haproxy/embed/request.lua', 33 | ['haproxy.embed.response'] = 'lua/haproxy/embed/response.lua', 34 | ['haproxy.embed.view'] = 'lua/haproxy/embed/view.lua', 35 | ['haproxy.process'] = 'lua/haproxy/process.lua', 36 | ['haproxy.service'] = 'lua/haproxy/service.lua', 37 | ['haproxy.stats'] = 'lua/haproxy/stats.lua', 38 | ['haproxy.util'] = 'lua/haproxy/util.lua', 39 | }, 40 | install = { 41 | bin = { 42 | ['lua-haproxy-api'] = 'lua/main.lua', 43 | }, 44 | }, 45 | copy_directories = { 46 | 'doc', 47 | 'spec', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /spec/fixtures/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "CompressBpsIn": 0, 3 | "CompressBpsOut": 0, 4 | "CompressBpsRateLim": 0, 5 | "ConnRate": 0, 6 | "ConnRateLimit": 0, 7 | "CumConns": 10, 8 | "CumReq": 10, 9 | "CurrConns": 0, 10 | "Hard_maxconn": 2000, 11 | "Idle_pct": 100, 12 | "MaxConnRate": 1, 13 | "MaxSessRate": 1, 14 | "Maxconn": 2000, 15 | "Maxpipes": 0, 16 | "Maxsock": 4031, 17 | "Memmax_MB": 0, 18 | "Name": "HAProxy", 19 | "Nbproc": 1, 20 | "Pid": 86316, 21 | "PipesFree": 0, 22 | "PipesUsed": 0, 23 | "Process_num": 1, 24 | "Release_date": "2015/12/25", 25 | "Run_queue": 1, 26 | "SessRate": 0, 27 | "SessRateLimit": 0, 28 | "Tasks": 5, 29 | "Ulimit-n": 4031, 30 | "Uptime": "0d 0h29m34s", 31 | "Uptime_sec": 1774, 32 | "Version": "1.6.3", 33 | "description": "", 34 | "node": "example" 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/info.txt: -------------------------------------------------------------------------------- 1 | Name: HAProxy 2 | Version: 1.6.3 3 | Release_date: 2015/12/25 4 | Nbproc: 1 5 | Process_num: 1 6 | Pid: 86316 7 | Uptime: 0d 0h29m34s 8 | Uptime_sec: 1774 9 | Memmax_MB: 0 10 | Ulimit-n: 4031 11 | Maxsock: 4031 12 | Maxconn: 2000 13 | Hard_maxconn: 2000 14 | CurrConns: 0 15 | CumConns: 10 16 | CumReq: 10 17 | Maxpipes: 0 18 | PipesUsed: 0 19 | PipesFree: 0 20 | ConnRate: 0 21 | ConnRateLimit: 0 22 | MaxConnRate: 1 23 | SessRate: 0 24 | SessRateLimit: 0 25 | MaxSessRate: 1 26 | CompressBpsIn: 0 27 | CompressBpsOut: 0 28 | CompressBpsRateLim: 0 29 | Tasks: 5 30 | Run_queue: 1 31 | Idle_pct: 100 32 | node: example 33 | description: 34 | 35 | -------------------------------------------------------------------------------- /spec/fixtures/stats.csv: -------------------------------------------------------------------------------- 1 | # pxname,svname,qcur,qmax,scur,smax,slim,stot,bin,bout,dreq,dresp,ereq,econ,eresp,wretr,wredis,status,weight,act,bck,chkfail,chkdown,lastchg,downtime,qlimit,pid,iid,sid,throttle,lbtot,tracked,type,rate,rate_lim,rate_max,check_status,check_code,check_duration,hrsp_1xx,hrsp_2xx,hrsp_3xx,hrsp_4xx,hrsp_5xx,hrsp_other,hanafail,req_rate,req_rate_max,req_tot,cli_abrt,srv_abrt,comp_in,comp_out,comp_byp,comp_rsp,lastsess,last_chk,last_agt,qtime,ctime,rtime,ttime, 2 | stats,FRONTEND,,,0,0,2000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,2,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,,0,0,0,0,,,,,,,, 3 | stats,localhost,0,0,0,0,,0,0,0,,0,,0,0,0,0,no check,1,1,0,,,,,,1,3,1,,0,,2,0,,0,,,,0,0,0,0,0,0,0,,,,0,0,,,,,-1,,,0,0,0,0, 4 | stats,BACKEND,0,0,0,0,1,0,0,0,0,0,,0,0,0,0,UP,1,1,0,,0,283,0,,1,3,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,0,0,0,0,-1,,,0,0,0,0, 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/stats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "act": "", 4 | "bck": "", 5 | "bin": 0, 6 | "bout": 0, 7 | "check_code": "", 8 | "check_duration": "", 9 | "check_status": "", 10 | "chkdown": "", 11 | "chkfail": "", 12 | "cli_abrt": "", 13 | "comp_byp": 0, 14 | "comp_in": 0, 15 | "comp_out": 0, 16 | "comp_rsp": 0, 17 | "downtime": "", 18 | "dreq": 0, 19 | "dresp": 0, 20 | "econ": "", 21 | "ereq": 0, 22 | "eresp": "", 23 | "hanafail": "", 24 | "hrsp_1xx": 0, 25 | "hrsp_2xx": 0, 26 | "hrsp_3xx": 0, 27 | "hrsp_4xx": 0, 28 | "hrsp_5xx": 0, 29 | "hrsp_other": 0, 30 | "iid": 2, 31 | "lastchg": "", 32 | "lbtot": "", 33 | "pid": 1, 34 | "pxname": "stats", 35 | "qcur": "", 36 | "qlimit": "", 37 | "qmax": "", 38 | "rate": 0, 39 | "rate_lim": 0, 40 | "rate_max": 0, 41 | "req_rate": 0, 42 | "req_rate_max": 0, 43 | "req_tot": 0, 44 | "scur": 0, 45 | "sid": 0, 46 | "slim": 2000, 47 | "smax": 0, 48 | "srv_abrt": "", 49 | "status": "OPEN", 50 | "stot": 0, 51 | "svname": "FRONTEND", 52 | "throttle": "", 53 | "tracked": "", 54 | "type": 0, 55 | "weight": "", 56 | "wredis": "", 57 | "wretr": "" 58 | }, 59 | { 60 | "act": 1, 61 | "bck": 0, 62 | "bin": 0, 63 | "bout": 0, 64 | "check_code": "", 65 | "check_duration": "", 66 | "check_status": "", 67 | "chkdown": "", 68 | "chkfail": "", 69 | "cli_abrt": 0, 70 | "comp_byp": "", 71 | "comp_in": "", 72 | "comp_out": "", 73 | "comp_rsp": "", 74 | "ctime": 0, 75 | "downtime": "", 76 | "dreq": "", 77 | "dresp": 0, 78 | "econ": 0, 79 | "ereq": "", 80 | "eresp": 0, 81 | "hanafail": 0, 82 | "hrsp_1xx": 0, 83 | "hrsp_2xx": 0, 84 | "hrsp_3xx": 0, 85 | "hrsp_4xx": 0, 86 | "hrsp_5xx": 0, 87 | "hrsp_other": 0, 88 | "iid": 3, 89 | "last_agt": "", 90 | "last_chk": "", 91 | "lastchg": "", 92 | "lastsess": -1, 93 | "lbtot": 0, 94 | "pid": 1, 95 | "pxname": "stats", 96 | "qcur": 0, 97 | "qlimit": "", 98 | "qmax": 0, 99 | "qtime": 0, 100 | "rate": 0, 101 | "rate_lim": "", 102 | "rate_max": 0, 103 | "req_rate": "", 104 | "req_rate_max": "", 105 | "req_tot": "", 106 | "rtime": 0, 107 | "scur": 0, 108 | "sid": 1, 109 | "slim": "", 110 | "smax": 0, 111 | "srv_abrt": 0, 112 | "status": "no check", 113 | "stot": 0, 114 | "svname": "localhost", 115 | "throttle": "", 116 | "tracked": "", 117 | "ttime": 0, 118 | "type": 2, 119 | "weight": 1, 120 | "wredis": 0, 121 | "wretr": 0 122 | }, 123 | { 124 | "act": 1, 125 | "bck": 0, 126 | "bin": 0, 127 | "bout": 0, 128 | "check_code": "", 129 | "check_duration": "", 130 | "check_status": "", 131 | "chkdown": 0, 132 | "chkfail": "", 133 | "cli_abrt": 0, 134 | "comp_byp": 0, 135 | "comp_in": 0, 136 | "comp_out": 0, 137 | "comp_rsp": 0, 138 | "ctime": 0, 139 | "downtime": 0, 140 | "dreq": 0, 141 | "dresp": 0, 142 | "econ": 0, 143 | "ereq": "", 144 | "eresp": 0, 145 | "hanafail": "", 146 | "hrsp_1xx": 0, 147 | "hrsp_2xx": 0, 148 | "hrsp_3xx": 0, 149 | "hrsp_4xx": 0, 150 | "hrsp_5xx": 0, 151 | "hrsp_other": 0, 152 | "iid": 3, 153 | "last_agt": "", 154 | "last_chk": "", 155 | "lastchg": 283, 156 | "lastsess": -1, 157 | "lbtot": 0, 158 | "pid": 1, 159 | "pxname": "stats", 160 | "qcur": 0, 161 | "qlimit": "", 162 | "qmax": 0, 163 | "qtime": 0, 164 | "rate": 0, 165 | "rate_lim": "", 166 | "rate_max": 0, 167 | "req_rate": "", 168 | "req_rate_max": "", 169 | "req_tot": "", 170 | "rtime": 0, 171 | "scur": 0, 172 | "sid": 0, 173 | "slim": 1, 174 | "smax": 0, 175 | "srv_abrt": 0, 176 | "status": "UP", 177 | "stot": 0, 178 | "svname": "BACKEND", 179 | "throttle": "", 180 | "tracked": "", 181 | "ttime": 0, 182 | "type": 1, 183 | "weight": 1, 184 | "wredis": 0, 185 | "wretr": 0 186 | } 187 | ] 188 | -------------------------------------------------------------------------------- /spec/haproxy_spec.lua: -------------------------------------------------------------------------------- 1 | local file = require('pl.file') 2 | local path = require('pl.path') 3 | local stringx = require('pl.stringx') 4 | local json = require('dkjson') 5 | 6 | local stats = require('haproxy.stats') 7 | 8 | local SPEC_DIR = path.abspath(path.dirname(stringx.lstrip(debug.getinfo(1, 'S').source, '@'))) 9 | 10 | local function fixture(name) 11 | return file.read(path.join(SPEC_DIR, 'fixtures', name)) 12 | end 13 | 14 | describe('Client', function() 15 | it('initializes correctly with default values', function() 16 | local client = stats.Client() 17 | assert.equal(getmetatable(client), stats.Client) 18 | end) 19 | 20 | it('accepts socket configuration', function() 21 | local client = stats.Client('127.0.0.1', 8000, 1) 22 | assert.same( 23 | { 24 | address = '127.0.0.1', 25 | port = 8000, 26 | timeout = 1, 27 | }, 28 | client) 29 | end) 30 | 31 | it('can be initialized with _init() or new()', function() 32 | local client1 = stats.Client() 33 | local client2 = stats.Client.new() 34 | assert.same(client1, client2) 35 | end) 36 | 37 | it('should use LuaSocket outside of HAProxy', function() 38 | local client = stats.Client() 39 | local socket = require('socket') 40 | assert.same(getmetatable(client.tcp()), getmetatable(socket.tcp())) 41 | end) 42 | end) 43 | 44 | describe('stats parser', function() 45 | it('should parse CSV correctly', function() 46 | local response = fixture('stats.csv') 47 | local parsed = json.decode(fixture('stats.json')) 48 | assert.same(stats.parse_stats(response), parsed) 49 | end) 50 | end) 51 | 52 | describe('info parser', function() 53 | it('should parse info correctly', function() 54 | local response = fixture('info.txt') 55 | local parsed = json.decode(fixture('info.json')) 56 | assert.same(stats.parse_info(response), parsed) 57 | end) 58 | end) 59 | -------------------------------------------------------------------------------- /vendor/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean cleanall 2 | 3 | HAPROXY_VERSION = 1.6.5 4 | HAPROXY = haproxy-$(HAPROXY_VERSION) 5 | HAPROXY_URL = http://www.haproxy.org/download/$(basename $(HAPROXY_VERSION)) 6 | HAPROXY_BUILD_FLAGS = USE_LUA=1 LUA_LIB=$(PWD)/lib LUA_INC=$(PWD)/include 7 | 8 | LUA_VERSION = 5.3.3 9 | LUA = lua-$(LUA_VERSION) 10 | LUA_URL = https://www.lua.org/ftp 11 | 12 | LUAROCKS_VERSION = 2.3.0 13 | LUAROCKS = luarocks-$(LUAROCKS_VERSION) 14 | LUAROCKS_URL = https://keplerproject.github.io/luarocks/releases 15 | 16 | LUA_AMALG_VERSION = 100d5e2c0203b76cc4969fd758cdf19d23fd4993 17 | LUA_AMALG = lua-amalg-$(LUA_AMALG_VERSION) 18 | LUA_AMALG_URL = https://github.com/siffiejoe/lua-amalg/archive/$(LUA_AMALG_VERSION).tar.gz 19 | 20 | UNAME := $(shell uname -s) 21 | PWD := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 22 | WGET = wget -nc 23 | MKDIR = mkdir -p 24 | 25 | ifeq ($(UNAME),Linux) 26 | LUA_PLATFORM = linux 27 | HAPROXY_TARGET = linux2628 28 | endif 29 | ifeq ($(UNAME),Darwin) 30 | LUA_PLATFORM = macosx 31 | HAPROXY_TARGET = osx 32 | # On OS X, libcrypt is built into unistd. 33 | HAPROXY_BUILD_FLAGS += USE_LIBCRYPT= 34 | # OS X uses BSD ld. --export-dynamic corresponds to -export_dynamic. 35 | HAPROXY_BUILD_FLAGS += LUA_LD_FLAGS="-Wl,-export_dynamic -L$(PWD)/lib" 36 | endif 37 | 38 | all: sbin/haproxy bin/luarocks bin/amalg.lua 39 | 40 | bin/amalg.lua: src/$(LUA_AMALG) 41 | cd src/$(LUA_AMALG) && $(PWD)/bin/luarocks install amalg-scm-0.rockspec 42 | 43 | bin/lua: src/$(LUA) 44 | $(MAKE) -C $< $(LUA_PLATFORM) 45 | $(MAKE) -C $< install INSTALL_TOP=$(PWD) 46 | 47 | bin/luarocks: src/$(LUAROCKS) 48 | cd src/$(LUAROCKS) && ./configure --prefix=$(PWD) --with-lua=$(PWD) --force-config 49 | $(MAKE) -C $< build 50 | $(MAKE) -C $< install 51 | 52 | clean: 53 | -$(MAKE) -C src/$(HAPROXY) clean 54 | -$(MAKE) -C src/$(LUA) clean 55 | -$(MAKE) -C src/$(LUAROCKS) clean 56 | $(RM) -r bin etc doc include lib man sbin share 57 | 58 | cleanall: clean 59 | $(RM) -r src 60 | 61 | sbin/haproxy: src/$(HAPROXY) bin/lua 62 | $(MAKE) -C $< TARGET=$(HAPROXY_TARGET) $(HAPROXY_BUILD_FLAGS) 63 | $(MAKE) -C $< install PREFIX= DESTDIR=$(PWD) 64 | 65 | src: 66 | $(MKDIR) src 67 | 68 | src/$(LUA_AMALG): %: %.tar.gz 69 | tar -xzf $< -C src 70 | 71 | src/$(LUA_AMALG).tar.gz: src 72 | $(WGET) -P $< -O $@ $(LUA_AMALG_URL) 73 | grep $@ SHA256SUMS | sha256sum -c 74 | 75 | src/$(HAPROXY): %: %.tar.gz 76 | tar -xzf $< -C src 77 | 78 | src/$(HAPROXY).tar.gz: src 79 | $(WGET) -P $< $(HAPROXY_URL)/$@ 80 | grep $@ SHA256SUMS | sha256sum -c 81 | 82 | src/$(LUA): %: %.tar.gz 83 | tar -xzf $< -C src 84 | 85 | src/$(LUA).tar.gz: src 86 | $(WGET) -P $< $(LUA_URL)/$(notdir $@) 87 | grep $@ SHA256SUMS | sha256sum -c 88 | 89 | src/$(LUAROCKS): %: %.tar.gz 90 | tar -xzf $< -C src 91 | 92 | src/$(LUAROCKS).tar.gz: src 93 | $(WGET) -P $< $(LUAROCKS_URL)/$@ 94 | grep $@ SHA256SUMS | sha256sum -c 95 | -------------------------------------------------------------------------------- /vendor/SHA256SUMS: -------------------------------------------------------------------------------- 1 | 94b6f60addb373859a1e53d0b9b47e8531ea908912859fcf6423f7a3104c9e3b src/lua-amalg-100d5e2c0203b76cc4969fd758cdf19d23fd4993.tar.gz 2 | c4b3fb938874abbbbd52782087117cc2590263af78fdce86d64e4a11acfe85de src/haproxy-1.6.5.tar.gz 3 | 5113c06884f7de453ce57702abaac1d618307f33f6789fa870e87a59d772aca2 src/lua-5.3.3.tar.gz 4 | 68e38feeb66052e29ad1935a71b875194ed8b9c67c2223af5f4d4e3e2464ed97 src/luarocks-2.3.0.tar.gz 5 | --------------------------------------------------------------------------------