Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3763955
D2793.id7130.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
35 KB
Referenced Files
None
Subscribers
None
D2793.id7130.diff
View Options
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,424 @@
+#!/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,
+ }
+ actual = {}
+ is_existing = False
+
+ if __salt__["rabbitmq_api.check_queue_binding"](
+ cluster, vhost, queue, exchange, routing_key=routing_key, arguments=arguments
+ ):
+ ret["result"] = True
+ ret["comment"] = f"Queue already bound"
+ return ret
+
+ ret["changes"] = expected
+
+ if __opts__["test"]:
+ ret["result"] = None
+ ret["comment"] = f"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: ¬ifications-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
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 23, 03:34 (16 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2257011
Default Alt Text
D2793.id7130.diff (35 KB)
Attached To
Mode
D2793: Provision RabbitMQ configuration
Attached
Detach File
Event Timeline
Log In to Comment