Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3769125
D2793.id7096.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
23 KB
Referenced Files
None
Subscribers
None
D2793.id7096.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
--- a/_modules/rabbitmq.py
+++ b/_modules/rabbitmq.py
@@ -19,6 +19,11 @@
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+def get_password(credential):
+ secret = __salt__["vault.read_secret"](credential)
+ return compute_password_hash(secret["password"])
+
+
def compute_password_hash(password):
salt = secrets.randbits(32)
return _compute_password_hash_with_salt(salt, password)
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,168 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Salt - RabbitMQ management HTTP API client
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Connect to RabbitMQ management HTTP API
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import json
+import logging
+
+
+import requests
+from requests.auth import HTTPBasicAuth
+
+
+log = logging.getLogger(__name__)
+
+
+HTTP_SUCCESS_CODES = [200, 201, 204]
+HTTP_CONTENT_CODES = [200]
+
+
+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_VHOST = ["description", "tags", "tracing"]
+ARGS_EXCHANGE = ["type", "auto_delete", "durable", "internal", "arguments"]
+ARGS_QUEUE = ["auto_delete", "durable", "arguments", "node"]
+
+
+def overview(cluster):
+ return _request(cluster, "GET", "overview")
+
+
+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)]
diff --git a/_states/rabbitmq.py b/_states/rabbitmq.py
new file mode 100644
--- /dev/null
+++ b/_states/rabbitmq.py
@@ -0,0 +1,243 @@
+#!/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__)
+
+
+# -------------------------------------------------------------
+# 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:
+ 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:
+ 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
+
+
+# -------------------------------------------------------------
+# Helper functions
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def _changes(actual, expected):
+ """Compute a changes dictionary between actual and expected state dictionaries."""
+ changes = {}
+ for k, v in expected.items():
+ if k not in actual:
+ changes[k] = {"old": None, "new": expected[k]}
+ elif actual[k] != expected[k]:
+ changes[k] = {"old": actual[k], "new": expected[k]}
+
+ return changes
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/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/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
Sun, Nov 24, 13:06 (10 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260210
Default Alt Text
D2793.id7096.diff (23 KB)
Attached To
Mode
D2793: Provision RabbitMQ configuration
Attached
Detach File
Event Timeline
Log In to Comment