Page MenuHomeDevCentral

D2793.id7131.diff
No OneTemporary

D2793.id7131.diff

diff --git a/.arclint b/.arclint
--- a/.arclint
+++ b/.arclint
@@ -47,6 +47,7 @@
],
"include": [
"(\\.py$)",
+ "(^roles/saas-rabbitmq/server/content.sls$)",
"(^roles/viperserv/eggdrop/cron.sls$)",
"(^roles/webserver-legacy/php-builder/source.sls$)",
"(^roles/webserver-legacy/php-sites/cleanup.sls$)"
diff --git a/_modules/rabbitmq.py b/_modules/rabbitmq.py
deleted file mode 100644
--- a/_modules/rabbitmq.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# -------------------------------------------------------------
-# Salt — RabbitMQ execution module
-# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-# Project: Nasqueron
-# Description: Allow to use RabbitMQ management plugin HTTP API
-# License: BSD-2-Clause
-# -------------------------------------------------------------
-
-
-import base64
-import hashlib
-import secrets
-
-
-# -------------------------------------------------------------
-# Credentials
-# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-def compute_password_hash(password):
- salt = secrets.randbits(32)
- return _compute_password_hash_with_salt(salt, password)
-
-
-def _compute_password_hash_with_salt(salt, password):
- """Reference: https://rabbitmq.com/passwords.html#computing-password-hash"""
- salt = salt.to_bytes(4, "big") # salt is a 32 bits (4 bytes) value
-
- m = hashlib.sha256()
- m.update(salt)
- m.update(password.encode("utf-8"))
- result = salt + m.digest()
-
- return base64.b64encode(result).decode("utf-8")
diff --git a/_modules/rabbitmq_api.py b/_modules/rabbitmq_api.py
new file mode 100644
--- /dev/null
+++ b/_modules/rabbitmq_api.py
@@ -0,0 +1,327 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Salt - RabbitMQ management HTTP API client
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Connect to RabbitMQ management HTTP API
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import base64
+import hashlib
+import json
+import logging
+import secrets
+
+import requests
+from requests.auth import HTTPBasicAuth
+
+
+log = logging.getLogger(__name__)
+
+
+HTTP_SUCCESS_CODES = [200, 201, 204]
+HTTP_CONTENT_CODES = [200]
+
+
+# -------------------------------------------------------------
+# RabbitMQ management HTTP API client
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def _request(cluster, method, path, data=None):
+ args = __opts__["rabbitmq"][cluster]
+
+ url = args["url"] + "/" + path
+
+ if args["auth"] == "basic":
+ auth = HTTPBasicAuth(args["user"], args["password"])
+ else:
+ raise RuntimeError(
+ f"RabbitMQ HTTP API authentication scheme not supported: {args['auth']}"
+ )
+
+ headers = {
+ "User-agent": "Salt-RabbitMQ/1.0",
+ }
+
+ if data is not None:
+ data = json.dumps(data)
+
+ log.debug(f"HTTP request {method} to {url}")
+ log.trace(f"Payload: {data}")
+ r = requests.request(method, url, headers=headers, auth=auth, data=data)
+
+ if r.status_code not in HTTP_SUCCESS_CODES:
+ log.error(f"HTTP status code {r.status_code}, 2xx expected.")
+ raise RuntimeError(f"Status code is {r.status_code}")
+
+ if r.status_code not in HTTP_CONTENT_CODES:
+ log.trace(
+ f"HTTP response is {r.status_code}. The API doesn't include any content for this code."
+ )
+ return True
+
+ return r.json()
+
+
+# -------------------------------------------------------------
+# Execution module methods
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+ARGS_USER = ["password_hash", "tags"]
+ARGS_VHOST = ["description", "tags", "tracing"]
+ARGS_EXCHANGE = ["type", "auto_delete", "durable", "internal", "arguments"]
+ARGS_QUEUE = ["auto_delete", "durable", "arguments", "node"]
+ARGS_BINDING = ["routing_key", "arguments"]
+
+
+def overview(cluster):
+ return _request(cluster, "GET", "overview")
+
+
+def list_users(cluster):
+ return _request(cluster, "GET", "users")
+
+
+def get_user(cluster, user):
+ user = requests.utils.quote(user, safe="")
+ return _request(cluster, "GET", f"users/{user}")
+
+
+def update_user(cluster, user, **kwargs):
+ user = requests.utils.quote(user, safe="")
+ data = {}
+ for arg in ARGS_USER:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ if "password" in kwargs:
+ if "password_hash" in kwargs:
+ raise RuntimeError(
+ "You can't specify both password and password_hash option."
+ )
+ data["password_hash"] = compute_password_hash(kwargs["password"])
+
+ return _request(cluster, "PUT", f"users/{user}", data)
+
+
+def delete_user(cluster, user):
+ user = requests.utils.quote(user, safe="")
+ return _request(cluster, "DELETE", f"users/{user}")
+
+
+def user_exists(cluster, user):
+ return user in [result["name"] for result in list_users(cluster)]
+
+
+def list_vhosts(cluster):
+ return _request(cluster, "GET", "vhosts")
+
+
+def get_vhost(cluster, vhost):
+ vhost = requests.utils.quote(vhost, safe="")
+ return _request(cluster, "GET", f"vhosts/{vhost}")
+
+
+def update_vhost(cluster, vhost, **kwargs):
+ vhost = requests.utils.quote(vhost, safe="")
+ data = {}
+ for arg in ARGS_VHOST:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ return _request(cluster, "PUT", f"vhosts/{vhost}", data)
+
+
+def delete_vhost(cluster, vhost):
+ vhost = requests.utils.quote(vhost, safe="")
+ return _request(cluster, "DELETE", f"vhosts/{vhost}")
+
+
+def vhost_exists(cluster, vhost):
+ return vhost in [result["name"] for result in list_vhosts(cluster)]
+
+
+def list_exchanges(cluster, vhost):
+ vhost = requests.utils.quote(vhost, safe="")
+ return _request(cluster, "GET", f"exchanges/{vhost}")
+
+
+def get_exchange(cluster, vhost, exchange):
+ vhost = requests.utils.quote(vhost, safe="")
+ exchange = requests.utils.quote(exchange, safe="")
+ return _request(cluster, "GET", f"exchanges/{vhost}/{exchange}")
+
+
+def update_exchange(cluster, vhost, exchange, **kwargs):
+ vhost = requests.utils.quote(vhost, safe="")
+ exchange = requests.utils.quote(exchange, safe="")
+ data = {}
+ for arg in ARGS_EXCHANGE:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ return _request(cluster, "PUT", f"exchanges/{vhost}/{exchange}", data)
+
+
+def delete_exchange(cluster, vhost, exchange):
+ vhost = requests.utils.quote(vhost, safe="")
+ exchange = requests.utils.quote(exchange, safe="")
+ return _request(cluster, "DELETE", f"exchanges/{vhost}/{exchange}")
+
+
+def exchange_exists(cluster, vhost, exchange):
+ vhost = requests.utils.quote(vhost, safe="")
+ return exchange in [result["name"] for result in list_exchanges(cluster, vhost)]
+
+
+def list_queues(cluster, vhost):
+ vhost = requests.utils.quote(vhost, safe="")
+ return _request(cluster, "GET", f"queues/{vhost}")
+
+
+def get_queue(cluster, vhost, queue):
+ vhost = requests.utils.quote(vhost, safe="")
+ queue = requests.utils.quote(queue, safe="")
+ return _request(cluster, "GET", f"queues/{vhost}/{queue}")
+
+
+def update_queue(cluster, vhost, queue, **kwargs):
+ vhost = requests.utils.quote(vhost, safe="")
+ queue = requests.utils.quote(queue, safe="")
+
+ data = {}
+ for arg in ARGS_QUEUE:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ return _request(cluster, "PUT", f"queues/{vhost}/{queue}", data)
+
+
+def delete_queue(cluster, vhost, queue):
+ vhost = requests.utils.quote(vhost, safe="")
+ queue = requests.utils.quote(queue, safe="")
+ return _request(cluster, "DELETE", f"queues/{vhost}/{queue}")
+
+
+def queue_exists(cluster, vhost, queue):
+ vhost = requests.utils.quote(vhost, safe="")
+ return queue in [result["name"] for result in list_queues(cluster, vhost)]
+
+
+def list_bindings(cluster, vhost):
+ vhost = requests.utils.quote(vhost, safe="")
+ return _request(cluster, "GET", f"bindings/{vhost}")
+
+
+def check_queue_binding(cluster, vhost, queue, exchange, **kwargs):
+ bindings = list_bindings(cluster, vhost)
+ for binding in bindings:
+ # The binding is the one we want if all fields are equal
+ if binding["source"] != exchange:
+ continue
+ if binding["destination_type"] != "queue":
+ continue
+ if binding["destination"] != queue:
+ continue
+ for arg in ARGS_BINDING:
+ if binding[arg] != kwargs[arg]:
+ continue
+
+ # We've got a winner
+ return True
+
+ return False
+
+
+def check_exchange_binding(cluster, vhost, source, destination, **kwargs):
+ bindings = list_bindings(cluster, vhost)
+ for binding in bindings:
+ # The binding is the one we want if all fields are equal
+ if binding["source"] != source:
+ continue
+ if binding["destination_type"] != "exchange":
+ continue
+ if binding["destination"] != destination:
+ continue
+ for arg in ARGS_BINDING:
+ if binding[arg] != kwargs[arg]:
+ continue
+
+ # We've got a winner
+ return True
+
+ return False
+
+
+def queue_bind(cluster, vhost, exchange, queue, **kwargs):
+ vhost = requests.utils.quote(vhost, safe="")
+ queue = requests.utils.quote(queue, safe="")
+ exchange = requests.utils.quote(exchange, safe="")
+
+ data = {}
+ for arg in ARGS_BINDING:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ return _request(cluster, "POST", f"bindings/{vhost}/e/{exchange}/q/{queue}", data)
+
+
+def exchange_bind(cluster, vhost, source, destination, **kwargs):
+ vhost = requests.utils.quote(vhost, safe="")
+ source = requests.utils.quote(source, safe="")
+ destination = requests.utils.quote(destination, safe="")
+
+ data = {}
+ for arg in ARGS_BINDING:
+ if arg in kwargs:
+ data[arg] = kwargs[arg]
+
+ return _request(
+ cluster, "POST", f"bindings/{vhost}/e/{source}/e/{destination}", data
+ )
+
+
+def get_permissions(cluster, vhost, user):
+ return _request(cluster, "GET", f"permissions/{vhost}/{user}")
+
+
+def update_permissions(cluster, vhost, user, **kwargs):
+ data = {
+ "configure": "^$",
+ "read": "^$",
+ "write": "^$",
+ }
+
+ for arg in data:
+ if arg in kwargs:
+ if kwargs[arg]:
+ data[arg] = kwargs[arg]
+
+ return _request(cluster, "PUT", f"permissions/{vhost}/{user}", data)
+
+
+# -------------------------------------------------------------
+# Credentials helper methods
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def compute_password_hash(password):
+ salt = secrets.randbits(32)
+ return compute_password_hash_with_salt(salt, password)
+
+
+def compute_password_hash_with_salt(salt, password):
+ """Reference: https://rabbitmq.com/passwords.html#computing-password-hash"""
+ salt = salt.to_bytes(4, "big") # salt is a 32 bits (4 bytes) value
+
+ m = hashlib.sha256()
+ m.update(salt)
+ m.update(password.encode("utf-8"))
+ result = salt + m.digest()
+
+ return base64.b64encode(result).decode("utf-8")
diff --git a/_states/rabbitmq.py b/_states/rabbitmq.py
new file mode 100644
--- /dev/null
+++ b/_states/rabbitmq.py
@@ -0,0 +1,422 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Salt - RabbitMQ management HTTP API state module
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Configure RabbitMQ through management HTTP API
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import logging
+
+
+log = logging.getLogger(__name__)
+
+
+# -------------------------------------------------------------
+# User
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def user_present(name, cluster, credential, tags=[]):
+ password_hash = _get_password_hash(credential)
+ return _user_present(name, cluster, password_hash=password_hash, tags=tags)
+
+
+def _user_present(name, cluster, password_hash=None, tags=[]):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = {
+ "password_hash": password_hash,
+ "tags": tags,
+ }
+ actual = {}
+ is_existing = False
+
+ if __salt__["rabbitmq_api.user_exists"](cluster, name):
+ user = __salt__["rabbitmq_api.get_user"](cluster, name)
+ is_existing = True
+ actual = {
+ "password_hash": user["password_hash"],
+ "tags": user["tags"],
+ }
+
+ if actual == expected:
+ ret["result"] = True
+ ret["comment"] = f"User {name} is up to date"
+ return ret
+
+ ret["changes"] = _changes(actual, expected)
+ update_verb = "updated" if is_existing else "created"
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"User {name} will be {update_verb}"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.update_user"](cluster, name, **expected)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't update RabbitMQ user: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = f"User {update_verb}"
+
+ return ret
+
+
+def user_absent(name, cluster):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ if not __salt__["rabbitmq_api.user_exists"](cluster, name):
+ ret["result"] = True
+ ret["comment"] = f"User {name} is absent"
+ return ret
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"User {name} will be deleted"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.delete_user"](cluster, name)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't delete RabbitMQ user: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = "User deleted"
+
+ return ret
+
+
+def user_permissions(name, cluster, vhost, user, permissions={}):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = permissions | {"vhost": vhost, "user": user}
+ actual = {}
+ is_existing = False
+
+ try:
+ actual = __salt__["rabbitmq_api.get_permissions"](cluster, vhost, user)
+ is_existing = True
+ except RuntimeError as e:
+ if "404" not in str(e):
+ raise e
+
+ if actual == expected:
+ ret["result"] = True
+ ret["comment"] = "Permission is up-to-date"
+ return ret
+
+ ret["changes"] = _changes(actual, expected)
+ update_verb = "updated" if is_existing else "created"
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"User permissions will be {update_verb}"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.update_permissions"](cluster, vhost, user, **permissions)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't update RabbitMQ user permissions: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = f"User permissions {update_verb}"
+
+ return ret
+
+
+# -------------------------------------------------------------
+# Vhost
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def vhost_present(name, cluster, description="", tags=[], tracing=False):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = {
+ "description": description,
+ "tags": tags,
+ "tracing": tracing,
+ }
+ actual = {}
+ is_existing = False
+
+ if __salt__["rabbitmq_api.vhost_exists"](cluster, name):
+ vhost = __salt__["rabbitmq_api.get_vhost"](cluster, name)
+ is_existing = True
+ actual = {
+ "description": vhost["metadata"]["description"],
+ "tags": vhost["metadata"]["tags"],
+ "tracing": vhost["tracing"],
+ }
+
+ if actual == expected:
+ ret["result"] = True
+ ret["comment"] = f"Vhost {name} is up to date"
+ return ret
+
+ ret["changes"] = _changes(actual, expected)
+ update_verb = "updated" if is_existing else "created"
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"Vhost {name} will be {update_verb}"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.update_vhost"](cluster, name, **expected)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't update RabbitMQ vhost: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = f"Vhost {update_verb}"
+
+ return ret
+
+
+def vhost_absent(name, cluster):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ if not __salt__["rabbitmq_api.vhost_exists"](cluster, name):
+ ret["result"] = True
+ ret["comment"] = f"Vhost {name} is absent"
+ return ret
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"Vhost {name} will be deleted"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.delete_vhost"](cluster, name)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't delete RabbitMQ vhost: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = "Vhost deleted"
+
+ return ret
+
+
+# -------------------------------------------------------------
+# Exchange
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def exchange_present(
+ name,
+ cluster,
+ vhost,
+ type,
+ auto_delete=False,
+ durable=False,
+ internal=False,
+ arguments={},
+):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = {
+ "type": type,
+ "auto_delete": auto_delete,
+ "durable": durable,
+ "internal": internal,
+ "arguments": arguments,
+ }
+ actual = {}
+ is_existing = False
+
+ if __salt__["rabbitmq_api.exchange_exists"](cluster, vhost, name):
+ exchange = __salt__["rabbitmq_api.get_exchange"](cluster, vhost, name)
+ is_existing = True
+ actual = {
+ "type": exchange["type"],
+ "auto_delete": exchange["auto_delete"],
+ "durable": exchange["durable"],
+ "internal": exchange["internal"],
+ "arguments": exchange["arguments"],
+ }
+
+ if actual == expected:
+ ret["result"] = True
+ ret["comment"] = f"Exchange {name} is up to date"
+ return ret
+
+ ret["changes"] = _changes(actual, expected)
+ update_verb = "deleted then created back" if is_existing else "created"
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"Exchange {name} will be {update_verb}"
+ return ret
+
+ try:
+ if is_existing:
+ operation = "delete"
+ __salt__["rabbitmq_api.delete_exchange"](cluster, vhost, name)
+
+ operation = "create"
+ __salt__["rabbitmq_api.update_exchange"](cluster, vhost, name, **expected)
+ except Exception as e:
+ e = str(e)
+ log.error(f"Can't {operation} RabbitMQ exchange: {e}")
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = f"Exchange {update_verb}"
+
+ return ret
+
+
+# -------------------------------------------------------------
+# Queue
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def queue_present(
+ name, cluster, vhost, auto_delete=False, durable=False, arguments={}, node=None
+):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = {
+ "auto_delete": auto_delete,
+ "durable": durable,
+ "arguments": arguments,
+ }
+ if node is not None:
+ expected["node"] = node
+
+ actual = {}
+ is_existing = False
+
+ if __salt__["rabbitmq_api.queue_exists"](cluster, vhost, name):
+ queue = __salt__["rabbitmq_api.get_queue"](cluster, vhost, name)
+ is_existing = True
+ actual = {
+ "auto_delete": queue["auto_delete"],
+ "durable": queue["durable"],
+ "arguments": queue["arguments"],
+ }
+ if node is not None:
+ actual["node"] = queue["node"]
+
+ if actual == expected:
+ ret["result"] = True
+ ret["comment"] = f"queue {name} is up to date"
+ return ret
+
+ ret["changes"] = _changes(actual, expected)
+ update_verb = "deleted then created back" if is_existing else "created"
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"queue {name} will be {update_verb}"
+ return ret
+
+ try:
+ if is_existing:
+ operation = "delete"
+ __salt__["rabbitmq_api.delete_queue"](cluster, vhost, name)
+
+ operation = "create"
+ __salt__["rabbitmq_api.update_queue"](cluster, vhost, name, **expected)
+ except Exception as e:
+ e = str(e)
+ log.error(f"Can't {operation} RabbitMQ queue: {e}")
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = f"queue {update_verb}"
+
+ return ret
+
+
+def queue_binding(name, cluster, vhost, queue, exchange, routing_key="#", arguments={}):
+ ret = {"name": name, "result": False, "changes": {}, "comment": ""}
+
+ expected = {
+ "routing_key": routing_key,
+ "arguments": arguments,
+ }
+
+ if __salt__["rabbitmq_api.check_queue_binding"](
+ cluster, vhost, queue, exchange, routing_key=routing_key, arguments=arguments
+ ):
+ ret["result"] = True
+ ret["comment"] = "Queue already bound"
+ return ret
+
+ ret["changes"] = expected
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = "Binding will be created"
+ return ret
+
+ try:
+ __salt__["rabbitmq_api.queue_bind"](cluster, vhost, exchange, queue, **expected)
+ except Exception as e:
+ e = str(e)
+ log.error("Can't create RabbitMQ binding: " + e)
+ ret["comment"] = e
+ return ret
+
+ ret["result"] = True
+ ret["comment"] = "Binding created"
+
+ return ret
+
+
+# -------------------------------------------------------------
+# Helper functions
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def _get_password_hash(credential):
+ secret = __salt__["vault.read_secret"](credential)
+ salt = int(secret["salt"], 16)
+ return __salt__["rabbitmq_api.compute_password_hash_with_salt"](
+ salt, secret["password"]
+ )
+
+
+def _changes(actual, expected):
+ """Compute a changes dictionary between actual and expected state dictionaries."""
+ changes = {}
+ for key, value in expected.items():
+ if key in actual and actual[key] == expected[key]:
+ continue
+
+ if "password" in key:
+ value = "*****"
+
+ if key not in actual:
+ old_value = None
+ elif "password" in key:
+ old_value = "*****"
+ else:
+ old_value = actual[key]
+
+ changes[key] = {"old": old_value, "new": value}
+
+ return changes
diff --git a/pillar/credentials/vault.sls b/pillar/credentials/vault.sls
--- a/pillar/credentials/vault.sls
+++ b/pillar/credentials/vault.sls
@@ -26,7 +26,7 @@
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
vault_policies_path: /srv/policies/vault
-vault_policies_source: salt://roles/vault/policies/files
+vault_policies_source: /srv/policies/vault
vault_mount_paths:
ops/secrets: ops/data/secrets
diff --git a/pillar/saas/rabbitmq.sls b/pillar/saas/rabbitmq.sls
new file mode 100644
--- /dev/null
+++ b/pillar/saas/rabbitmq.sls
@@ -0,0 +1,105 @@
+# -------------------------------------------------------------
+# Salt — RabbitMQ
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+# -------------------------------------------------------------
+# RabbitMQ clusters
+#
+# Each cluster is defined by a deployment method (e.g. docker),
+# and the node we can use to configure it.
+#
+# The cluster configuration is a collection of vhosts and users:
+#
+# vhosts:
+# <vhost name>: <configuration>
+#
+# users:
+# <user>: <password FULL secret path in Vault>
+#
+# In addition, a root account is managed by deployment states.
+#
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# The vhost configuration allows to define the exchanges and queues,
+# and the permissions users have on them.
+#
+# exchanges:
+# type is 'direct', 'topic' or 'fanout'
+#
+# queues:
+# Application can create their own ephemeral queue.
+# For that, it needs configure permission on the vhost.
+#
+# If an application needs a stable one, it should be configured here,
+# so we can drop the configure permission.
+#
+# permissions:
+# See https://www.rabbitmq.com/access-control.html#authorisation
+# for the needed permissions for an AMQP operation
+#
+# To give access to server-generated queue names, use amq\.gen.*
+# To not give any access, use blank string
+#
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+rabbitmq_clusters:
+ white-rabbit:
+ deployment: docker
+ node: docker-002
+ container: white-rabbit
+ url: https://white-rabbit.nasqueron.org/
+
+ vhosts:
+
+ ###
+ ### Nasqueron dev services:
+ ### - Notifications center
+ ###
+
+ dev: &nasqueron-dev-services-vhost
+ description: Nasqueron dev services
+
+ exchanges:
+ # Producer: Notifications center
+ # Consumers: any notifications client
+ notifications:
+ type: topic
+
+ queues:
+ # Used by Wearg to stream notifications to IRC
+ wearg-notifications: {}
+
+ bindings:
+ - exchange: notifications
+ queue: wearg-notifications
+ routing_key: '#'
+
+ permissions:
+ # Notifications center (paas-docker role / notifications container)
+ notifications:
+ configure: '^$'
+ read: '^$'
+ write: '^notifications$'
+
+ # Wearg (viperserv role)
+ wearg:
+ configure: '^$'
+ read: '^wearg\-notifications$'
+ write: '^$'
+
+ # Notifications CLI clients
+ notifications-ysul: &notifications-client-permissions
+ configure: '^amq\.gen.*$'
+ read: '^(amq\.gen.*|notifications)$'
+ write: '^amq\.gen.*$'
+ notifications-windriver: *notifications-client-permissions
+
+ users:
+ # Notifications center server and clients
+ notifications: ops/secrets/nasqueron.notifications.broker
+ wearg: apps/viperserv/broker
+ notifications-ysul: ops/secrets/nasqueron/notifications/notifications-cli/ysul
+ notifications-windriver: ops/secrets/nasqueron/notifications/notifications-cli/windriver
diff --git a/pillar/top.sls b/pillar/top.sls
--- a/pillar/top.sls
+++ b/pillar/top.sls
@@ -25,6 +25,9 @@
complector:
- credentials.vault
+ # To provision services
+ - saas.rabbitmq
+
docker-001:
- credentials.zr
- notifications.config
diff --git a/roles/saas-rabbitmq/init.sls b/roles/saas-rabbitmq/init.sls
new file mode 100644
--- /dev/null
+++ b/roles/saas-rabbitmq/init.sls
@@ -0,0 +1,9 @@
+# -------------------------------------------------------------
+# Salt — RabbitMQ
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+include:
+ - .server
diff --git a/roles/saas-rabbitmq/server/content.sls b/roles/saas-rabbitmq/server/content.sls
new file mode 100644
--- /dev/null
+++ b/roles/saas-rabbitmq/server/content.sls
@@ -0,0 +1,141 @@
+#!py
+
+# -------------------------------------------------------------
+# Salt — RabbitMQ
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# If eligible, licensed under BSD-2-Clause
+# -------------------------------------------------------------
+
+
+# -------------------------------------------------------------
+# Configuration provider
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def run():
+ config = {}
+
+ for cluster, cluster_args in __pillar__["rabbitmq_clusters"].items():
+ config |= configure_cluster(cluster, cluster_args)
+
+ return config
+
+
+# -------------------------------------------------------------
+# Cluster configuration
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def configure_cluster(cluster, cluster_args):
+ config = {}
+
+ for user, credential in cluster_args["users"].items():
+ id = f"rabbitmq_cluster_{cluster}_user_{user}"
+ config[id] = configure_user(cluster, user, credential)
+
+ for vhost, vhost_args in cluster_args["vhosts"].items():
+ id = f"rabbitmq_cluster_{cluster}_vhost_{vhost}"
+ config[id] = configure_vhost(cluster, vhost, vhost_args)
+
+ for exchange, exchange_args in vhost_args.get("exchanges", {}).items():
+ id = f"rabbitmq_cluster_{cluster}_vhost_{vhost}_exchange_{exchange}"
+ config[id] = configure_exchange(cluster, vhost, exchange, exchange_args)
+
+ for queue, queue_args in vhost_args.get("queues", {}).items():
+ id = f"rabbitmq_cluster_{cluster}_vhost_{vhost}_queue_{queue}"
+ config[id] = configure_queue(cluster, vhost, queue, queue_args)
+
+ i = 0
+ for binding in vhost_args.get("bindings", []):
+ i += 1
+ id = f"rabbitmq_cluster_{cluster}_vhost_{vhost}_binding_{i}"
+ config[id] = configure_binding(cluster, vhost, binding)
+
+ for user, permission in vhost_args.get("permissions", {}).items():
+ id = f"rabbitmq_cluster_{cluster}_vhost_{vhost}_permissions_user_{user}"
+ config[id] = configure_user_permission(cluster, vhost, user, permission)
+
+ return config
+
+
+# -------------------------------------------------------------
+# RabbitMQ vhosts
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def configure_vhost(cluster, vhost, vhost_args):
+ return {
+ "rabbitmq.vhost_present": [
+ {"name": vhost},
+ {"cluster": cluster},
+ {"description": vhost_args.get("description", "")},
+ ]
+ }
+
+
+# -------------------------------------------------------------
+# RabbitMQ exchanges and queues
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def configure_exchange(cluster, vhost, exchange, exchange_args):
+ return {
+ "rabbitmq.exchange_present": [
+ {"name": exchange},
+ {"cluster": cluster},
+ {"vhost": vhost},
+ {"type": exchange_args["type"]},
+ ]
+ }
+
+
+def configure_queue(cluster, vhost, queue, queue_args):
+ return {
+ "rabbitmq.queue_present": [
+ {"name": queue},
+ {"cluster": cluster},
+ {"vhost": vhost},
+ ]
+ }
+
+
+def configure_binding(cluster, vhost, binding):
+ params = [
+ {"queue": binding["queue"]},
+ {"cluster": cluster},
+ {"vhost": vhost},
+ {"exchange": binding["exchange"]},
+ ]
+
+ if "routing_key" in binding:
+ params.append({"routing_key": binding["routing_key"]})
+
+ return {"rabbitmq.queue_binding": params}
+
+
+# -------------------------------------------------------------
+# RabbitMQ users and permissions
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def configure_user(cluster, user, credential):
+ return {
+ "rabbitmq.user_present": [
+ {"name": user},
+ {"cluster": cluster},
+ {"credential": credential},
+ ]
+ }
+
+
+def configure_user_permission(cluster, vhost, user, privilege):
+ return {
+ "rabbitmq.user_permissions": [
+ {"cluster": cluster},
+ {"vhost": vhost},
+ {"user": user},
+ {"permissions": privilege},
+ ]
+ }
diff --git a/roles/saas-rabbitmq/server/init.sls b/roles/saas-rabbitmq/server/init.sls
new file mode 100644
--- /dev/null
+++ b/roles/saas-rabbitmq/server/init.sls
@@ -0,0 +1,12 @@
+# -------------------------------------------------------------
+# Salt — RabbitMQ
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+include:
+ - .software
+
+ # Content includes vhosts, exchanges, queues, users, privileges
+ - .content
diff --git a/roles/saas-rabbitmq/server/software.sls b/roles/saas-rabbitmq/server/software.sls
new file mode 100644
--- /dev/null
+++ b/roles/saas-rabbitmq/server/software.sls
@@ -0,0 +1,8 @@
+# -------------------------------------------------------------
+# Salt — RabbitMQ
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+# This state is left intentionally blank.
diff --git a/roles/vault/policies/files/salt-primary.hcl b/roles/vault/policies/files/salt-primary.hcl
--- a/roles/vault/policies/files/salt-primary.hcl
+++ b/roles/vault/policies/files/salt-primary.hcl
@@ -71,3 +71,18 @@
path "transit/keys/*"{
capabilities = ["create"]
}
+
+# -------------------------------------------------------------
+# RabbitMQ credentials
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% for cluster, cluster_args in pillar.get("rabbitmq_clusters", {}).items() %}
+# Cluster: {{ cluster }}
+
+{% for user, credential in cluster_args.get("users", {}).items() %}
+path "{{ credential.replace("/", "/data/", 1) }}" {
+ capabilities = [ "read" ]
+}
+{% endfor %}
+
+{% endfor %}
diff --git a/roles/vault/policies/init.sls b/roles/vault/policies/init.sls
--- a/roles/vault/policies/init.sls
+++ b/roles/vault/policies/init.sls
@@ -25,6 +25,7 @@
{{ policy_path }}:
file.managed:
- source: salt://roles/vault/policies/files/{{ policy }}.hcl
+ - template: jinja
vault_policy_{{ policy }}:
credentials.vault_policy_present:
diff --git a/services.sls b/services.sls
new file mode 100644
--- /dev/null
+++ b/services.sls
@@ -0,0 +1,14 @@
+# -------------------------------------------------------------
+# Salt configuration for Nasqueron servers :: services
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: List of the roles configured through services API.
+# They are typically run on the Salt primary server,
+# especially as they can need Vault credentials,
+# but they don't touch any file *directly*.
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+base:
+ 'local':
+ - roles/saas-rabbitmq

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 18, 20:30 (18 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2251141
Default Alt Text
D2793.id7131.diff (35 KB)

Event Timeline