dash 4.1.3 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dash/autoscale/connections/builder.rb +51 -0
  3. data/lib/dash/autoscale/connections.rb +102 -0
  4. data/lib/dash/autoscale/controller.rb +296 -0
  5. data/lib/dash/autoscale/cron.rb +75 -0
  6. data/lib/dash/autoscale/decision.rb +89 -0
  7. data/lib/dash/autoscale/duration.rb +19 -0
  8. data/lib/dash/autoscale/evaluation.rb +52 -0
  9. data/lib/dash/autoscale/guard.rb +48 -0
  10. data/lib/dash/autoscale/lease.rb +55 -0
  11. data/lib/dash/autoscale/lease_held.rb +2 -0
  12. data/lib/dash/autoscale/lease_lost.rb +2 -0
  13. data/lib/dash/autoscale/member.rb +28 -0
  14. data/lib/dash/autoscale/metrics.rb +70 -0
  15. data/lib/dash/autoscale/metrics_server.rb +71 -0
  16. data/lib/dash/autoscale/observation.rb +41 -0
  17. data/lib/dash/autoscale/pause.rb +34 -0
  18. data/lib/dash/autoscale/policy.rb +198 -0
  19. data/lib/dash/autoscale/pool.rb +103 -0
  20. data/lib/dash/autoscale/prometheus_client.rb +76 -0
  21. data/lib/dash/autoscale/provider/base.rb +43 -0
  22. data/lib/dash/autoscale/provider/exec.rb +94 -0
  23. data/lib/dash/autoscale/provider/upcloud.rb +140 -0
  24. data/lib/dash/autoscale/provider.rb +9 -0
  25. data/lib/dash/autoscale/provider_error.rb +4 -0
  26. data/lib/dash/autoscale/read_only_query.rb +38 -0
  27. data/lib/dash/autoscale/reader.rb +17 -0
  28. data/lib/dash/autoscale/recorder.rb +13 -0
  29. data/lib/dash/autoscale/role_state.rb +110 -0
  30. data/lib/dash/autoscale/sample.rb +13 -0
  31. data/lib/dash/autoscale/signal/pgbus.rb +19 -0
  32. data/lib/dash/autoscale/signal/prometheus.rb +8 -0
  33. data/lib/dash/autoscale/signal/sidekiq.rb +49 -0
  34. data/lib/dash/autoscale/signal.rb +31 -0
  35. data/lib/dash/autoscale/simulation.rb +200 -0
  36. data/lib/dash/autoscale/state_store.rb +132 -0
  37. data/lib/dash/autoscale/timestamp.rb +15 -0
  38. data/lib/dash/autoscale/tunnel.rb +48 -0
  39. data/lib/dash/cli/accessory.rb +16 -0
  40. data/lib/dash/cli/app/boot.rb +69 -44
  41. data/lib/dash/cli/app/boot_state.rb +70 -0
  42. data/lib/dash/cli/app/rollout_boot.rb +28 -9
  43. data/lib/dash/cli/app/running_targets.rb +39 -0
  44. data/lib/dash/cli/app/slot_version.rb +29 -0
  45. data/lib/dash/cli/app.rb +89 -32
  46. data/lib/dash/cli/autoscale.rb +312 -0
  47. data/lib/dash/cli/base.rb +15 -3
  48. data/lib/dash/cli/lock.rb +6 -1
  49. data/lib/dash/cli/main.rb +77 -8
  50. data/lib/dash/cli/proxy/inspection.rb +52 -0
  51. data/lib/dash/cli/proxy/loadbalancer_deploy.rb +20 -0
  52. data/lib/dash/cli/proxy/loadbalancer_reboot.rb +1 -1
  53. data/lib/dash/cli/proxy/reboot.rb +4 -9
  54. data/lib/dash/cli/proxy.rb +12 -7
  55. data/lib/dash/cli/prune.rb +5 -3
  56. data/lib/dash/cli/report.rb +4 -1
  57. data/lib/dash/cli/scale/host_plan.rb +63 -0
  58. data/lib/dash/cli/scale/join.rb +128 -0
  59. data/lib/dash/cli/scale/leave.rb +58 -0
  60. data/lib/dash/cli/scale/placement.rb +54 -0
  61. data/lib/dash/cli/scale/power_off.rb +26 -0
  62. data/lib/dash/cli/scale/replica_join.rb +74 -0
  63. data/lib/dash/cli/scale/replica_leave.rb +121 -0
  64. data/lib/dash/cli/scale.rb +276 -0
  65. data/lib/dash/cli/server.rb +41 -0
  66. data/lib/dash/cli/stats_table.rb +34 -0
  67. data/lib/dash/cli/templates/sample_hooks/post-scale-in.sample +3 -0
  68. data/lib/dash/cli/templates/sample_hooks/post-scale-out.sample +3 -0
  69. data/lib/dash/cli/templates/sample_hooks/pre-scale-in.sample +6 -0
  70. data/lib/dash/cli/templates/sample_hooks/pre-scale-out.sample +6 -0
  71. data/lib/dash/commander.rb +17 -3
  72. data/lib/dash/commands/accessory/proxy.rb +1 -1
  73. data/lib/dash/commands/accessory.rb +21 -2
  74. data/lib/dash/commands/app/containers.rb +28 -3
  75. data/lib/dash/commands/app/logging.rb +13 -2
  76. data/lib/dash/commands/app/proxy.rb +4 -4
  77. data/lib/dash/commands/app.rb +90 -10
  78. data/lib/dash/commands/auditor.rb +2 -2
  79. data/lib/dash/commands/autoscale.rb +133 -0
  80. data/lib/dash/commands/base.rb +4 -0
  81. data/lib/dash/commands/proxy/services.rb +10 -0
  82. data/lib/dash/commands/prune.rb +10 -2
  83. data/lib/dash/commands/server.rb +16 -0
  84. data/lib/dash/configuration/accessory.rb +2 -2
  85. data/lib/dash/configuration/autoscale.rb +123 -0
  86. data/lib/dash/configuration/docs/autoscale.yml +141 -0
  87. data/lib/dash/configuration/docs/configuration.yml +8 -2
  88. data/lib/dash/configuration/docs/proxy.yml +1 -1
  89. data/lib/dash/configuration/docs/role.yml +203 -0
  90. data/lib/dash/configuration/loadbalancer.rb +3 -2
  91. data/lib/dash/configuration/proxy/run.rb +1 -1
  92. data/lib/dash/configuration/proxy.rb +19 -7
  93. data/lib/dash/configuration/role/drain.rb +49 -0
  94. data/lib/dash/configuration/role/replicas.rb +72 -0
  95. data/lib/dash/configuration/role/scale/guard.rb +40 -0
  96. data/lib/dash/configuration/role/scale/signal.rb +93 -0
  97. data/lib/dash/configuration/role/scale/window.rb +37 -0
  98. data/lib/dash/configuration/role/scale.rb +199 -0
  99. data/lib/dash/configuration/role.rb +123 -6
  100. data/lib/dash/configuration/validator/autoscale.rb +126 -0
  101. data/lib/dash/configuration/validator/proxy.rb +13 -0
  102. data/lib/dash/configuration/validator/role.rb +26 -5
  103. data/lib/dash/configuration.rb +126 -15
  104. data/lib/dash/diagnostics/audit.rb +28 -0
  105. data/lib/dash/diagnostics/autoscale_decisions.rb +18 -0
  106. data/lib/dash/diagnostics/autoscale_explain.rb +30 -0
  107. data/lib/dash/diagnostics/autoscale_signal.rb +82 -0
  108. data/lib/dash/diagnostics/base.rb +53 -0
  109. data/lib/dash/diagnostics/config.rb +32 -0
  110. data/lib/dash/diagnostics/container_stats.rb +67 -0
  111. data/lib/dash/diagnostics/containers.rb +20 -0
  112. data/lib/dash/diagnostics/controller_status.rb +44 -0
  113. data/lib/dash/diagnostics/docker_ps.rb +40 -0
  114. data/lib/dash/{cli → diagnostics}/doctor/config_checks.rb +3 -3
  115. data/lib/dash/diagnostics/doctor/controller_checks.rb +46 -0
  116. data/lib/dash/{cli → diagnostics}/doctor/endpoint_checks.rb +12 -4
  117. data/lib/dash/{cli → diagnostics}/doctor/host_checks.rb +5 -4
  118. data/lib/dash/diagnostics/doctor/pool_checks.rb +65 -0
  119. data/lib/dash/diagnostics/doctor/signal_checks.rb +54 -0
  120. data/lib/dash/diagnostics/doctor.rb +182 -0
  121. data/lib/dash/diagnostics/drift.rb +168 -0
  122. data/lib/dash/diagnostics/host_stats.rb +59 -0
  123. data/lib/dash/diagnostics/lines.rb +14 -0
  124. data/lib/dash/diagnostics/lock.rb +27 -0
  125. data/lib/dash/diagnostics/logs.rb +76 -0
  126. data/lib/dash/diagnostics/pool.rb +41 -0
  127. data/lib/dash/diagnostics/proxy_services.rb +36 -0
  128. data/lib/dash/diagnostics/redactor.rb +48 -0
  129. data/lib/dash/diagnostics/reports.rb +17 -0
  130. data/lib/dash/diagnostics/scale.rb +51 -0
  131. data/lib/dash/diagnostics/units.rb +31 -0
  132. data/lib/dash/mcp/base_tool.rb +43 -0
  133. data/lib/dash/mcp/runner.rb +28 -0
  134. data/lib/dash/mcp/server.rb +40 -0
  135. data/lib/dash/mcp/session.rb +51 -0
  136. data/lib/dash/mcp/tools/audit_tool.rb +11 -0
  137. data/lib/dash/mcp/tools/autoscale_decisions_tool.rb +22 -0
  138. data/lib/dash/mcp/tools/autoscale_explain_tool.rb +56 -0
  139. data/lib/dash/mcp/tools/config_tool.rb +10 -0
  140. data/lib/dash/mcp/tools/container_stats_tool.rb +15 -0
  141. data/lib/dash/mcp/tools/containers_tool.rb +10 -0
  142. data/lib/dash/mcp/tools/controller_status_tool.rb +18 -0
  143. data/lib/dash/mcp/tools/deploy_reports_tool.rb +10 -0
  144. data/lib/dash/mcp/tools/doctor_tool.rb +13 -0
  145. data/lib/dash/mcp/tools/drift_tool.rb +16 -0
  146. data/lib/dash/mcp/tools/host_stats_tool.rb +14 -0
  147. data/lib/dash/mcp/tools/lock_status_tool.rb +15 -0
  148. data/lib/dash/mcp/tools/logs_tool.rb +41 -0
  149. data/lib/dash/mcp/tools/pool_members_tool.rb +15 -0
  150. data/lib/dash/mcp/tools/proxy_services_tool.rb +10 -0
  151. data/lib/dash/mcp/tools/scale_status_tool.rb +10 -0
  152. data/lib/dash/mcp/tools/signal_sample_tool.rb +19 -0
  153. data/lib/dash/mcp/transport.rb +14 -0
  154. data/lib/dash/mcp.rb +56 -0
  155. data/lib/dash/sshkit_with_ext.rb +4 -4
  156. data/lib/dash/utils.rb +7 -0
  157. data/lib/dash/version.rb +1 -1
  158. data/lib/dash.rb +2 -0
  159. metadata +119 -5
  160. data/lib/dash/cli/doctor.rb +0 -113
@@ -0,0 +1,56 @@
1
+ class Dash::Mcp::Tools::AutoscaleExplainTool < Dash::Mcp::BaseTool
2
+ tool_name "autoscale_explain"
3
+ title "Autoscale explain"
4
+ description <<~DESC
5
+ One live evaluation of a scaled role by the autoscale policy, as the controller's next tick would make it, without acting:
6
+ the action (scale_out, scale_in, replace_member, hold), the container counts from and to, the reason codes (schedule_floor,
7
+ at_min, at_max, at_target, cooldown, warming_up, paused, lock_busy, member_unreachable, host_unreachable, pool_unreadable,
8
+ action_failed, above_threshold, below_threshold, held_by_guard, missing_data), when a hold can next change (eligible_at),
9
+ and every input: running count, floor, min/max bounds, step, active schedule windows, cooldowns, warmup and boot timeout,
10
+ last scale times, warming and unreachable members, unreadable baseline hosts, pause, and for a role with a signal its
11
+ sample, its guards' samples, since when it has been above or below its band, and the band itself. controlled is false
12
+ for a role without a schedule or a signal, which the controller leaves alone.
13
+ DESC
14
+ input_schema properties: { role: { type: "string", description: "The scaled role" } }, required: [ "role" ], additionalProperties: false
15
+
16
+ def self.call(server_context:, role:)
17
+ answer(server_context) do
18
+ ensure_state_host_in_scope
19
+ Dash::Diagnostics::AutoscaleExplain.new(role: scoped_role(role)).to_h
20
+ end
21
+ end
22
+
23
+ def self.scoped_role(name)
24
+ role = DASH.config.role(name) || raise(ArgumentError, "No role named #{name} (#{DASH.config.roles.map(&:name).join(",")})")
25
+ raise ArgumentError, "#{name} is outside this server's --roles and --hosts" unless roles_in_scope.include?(role)
26
+
27
+ role
28
+ end
29
+
30
+ # The roles within --roles, and with a baseline host within --hosts - from deploy.yml,
31
+ # never DASH.roles: that would ask the provider for the pool, and a pool that does not
32
+ # answer is an answer explain should give.
33
+ def self.roles_in_scope
34
+ roles = DASH.specific_roles || DASH.config.roles
35
+ DASH.specific_hosts ? roles.select { |role| (role.baseline_hosts & DASH.specific_hosts).any? } : roles
36
+ end
37
+
38
+ # nil when nothing narrows the server: every role's decisions.
39
+ def self.role_names_in_scope
40
+ roles_in_scope.map(&:name) if DASH.specific_roles || DASH.specific_hosts
41
+ end
42
+
43
+ # The controller's state host (Dash::Autoscale::StateStore.host) must be within the
44
+ # server's --hosts, or the hosts of its --roles (as lock_status asks) - when it is one of
45
+ # the app's hosts. The ceiling is over those: an `autoscale.controller` ops host that is
46
+ # none of them is outside its reach, and always answers. Baseline hosts only, never the
47
+ # pool, which a scope check must not ask.
48
+ def self.ensure_state_host_in_scope
49
+ host = Dash::Autoscale::StateStore.host
50
+ return unless (DASH.config.roles.flat_map(&:baseline_hosts) + DASH.config.accessories.flat_map(&:hosts)).include?(host)
51
+
52
+ hosts = DASH.specific_hosts || DASH.specific_roles&.flat_map(&:baseline_hosts)
53
+
54
+ raise ArgumentError, "The autoscale state lives on #{host}, outside this server's --hosts and --roles" if hosts && !hosts.include?(host)
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ class Dash::Mcp::Tools::ConfigTool < Dash::Mcp::BaseTool
2
+ tool_name "config"
3
+ title "Configuration"
4
+ description "The effective deploy configuration (redacted) and its topology: which hosts each role runs on, which roles are proxied, the proxy hosts and the load balancer. Local, no SSH."
5
+ input_schema properties: {}, additionalProperties: false
6
+
7
+ def self.call(server_context:)
8
+ answer(server_context) { Dash::Diagnostics::Config.new.to_h }
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ class Dash::Mcp::Tools::ContainerStatsTool < Dash::Mcp::BaseTool
2
+ tool_name "container_stats"
3
+ title "Container resource usage"
4
+ description <<~DESC
5
+ CPU %, memory and its limit, network and block I/O, and PIDs of every running container of this service per app host,
6
+ with role, replica slot and version - and under `accessories`, the same for each accessory's container on its own hosts.
7
+ A point-in-time sample: docker stats measures over about a second per host.
8
+ No argument reaches the command; hosts and roles only choose which configured hosts are asked.
9
+ DESC
10
+ input_schema properties: SCOPE, additionalProperties: false
11
+
12
+ def self.call(server_context:, hosts: nil, roles: nil)
13
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::ContainerStats.new.to_h }
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ class Dash::Mcp::Tools::ContainersTool < Dash::Mcp::BaseTool
2
+ tool_name "containers"
3
+ title "Containers"
4
+ description "Every container of this service per app host: role, replica slot, version, state, health, image and creation time."
5
+ input_schema properties: SCOPE, additionalProperties: false
6
+
7
+ def self.call(server_context:, hosts: nil, roles: nil)
8
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::Containers.new.to_h }
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ class Dash::Mcp::Tools::ControllerStatusTool < Dash::Mcp::BaseTool
2
+ tool_name "controller_status"
3
+ title "Autoscale controller"
4
+ description <<~DESC
5
+ Whether the autoscale controller (dash autoscale run) is running: its heartbeat - controller id, host, pid, dash version,
6
+ mode, interval, last tick and its age - read as running, stale (no tick for three intervals) or stopped; null when none ever ran.
7
+ Also the roles with a schedule or signal it controls, and the pauses in force (role, until, by whom).
8
+ DESC
9
+ input_schema properties: {}, additionalProperties: false
10
+
11
+ def self.call(server_context:)
12
+ answer(server_context) do
13
+ Dash::Mcp::Tools::AutoscaleExplainTool.ensure_state_host_in_scope
14
+
15
+ Dash::Diagnostics::ControllerStatus.new.to_h
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ class Dash::Mcp::Tools::DeployReportsTool < Dash::Mcp::BaseTool
2
+ tool_name "deploy_reports"
3
+ title "Deploy reports"
4
+ description "The saved deploy reports for this destination, most recent first: command, status, runtime, per-phase timings and advice. Local, no SSH."
5
+ input_schema properties: { last: { type: "integer", minimum: 1, maximum: Dash::Diagnostics::Reports::MAX, description: "How many reports (default 1)" } }, additionalProperties: false
6
+
7
+ def self.call(server_context:, last: 1)
8
+ answer(server_context) { Dash::Diagnostics::Reports.new(last: last).to_h }
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ class Dash::Mcp::Tools::DoctorTool < Dash::Mcp::BaseTool
2
+ tool_name "doctor"
3
+ title "Doctor"
4
+ description <<~DESC
5
+ dash doctor's readiness checks as {check, target, status, detail}: SSH, Docker, proxy image and version, ports, DNS,
6
+ certificates, readiness gates and drift. The registry check is skipped: it runs docker login on each host.
7
+ DESC
8
+ input_schema properties: SCOPE, additionalProperties: false
9
+
10
+ def self.call(server_context:, hosts: nil, roles: nil)
11
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::Doctor.new(registry: false).to_h }
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ class Dash::Mcp::Tools::DriftTool < Dash::Mcp::BaseTool
2
+ tool_name "drift"
3
+ title "Drift"
4
+ description <<~DESC
5
+ Whether the proxy pool is consistent with what runs: compares containers with dash-proxy and load balancer targets.
6
+ Each mismatch has a code: proxy_target_not_running, running_not_targeted, loadbalancer_target_missing,
7
+ loadbalancer_target_extra, version_mismatch, multiple_running_versions, member_not_targeted (a started pool member
8
+ the load balancer does not forward to), member_orphan (a started pool member running nothing of its role).
9
+ Version codes are skipped while the deploy lock is held.
10
+ DESC
11
+ input_schema properties: SCOPE, additionalProperties: false
12
+
13
+ def self.call(server_context:, hosts: nil, roles: nil)
14
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::Drift.take.to_h }
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ class Dash::Mcp::Tools::HostStatsTool < Dash::Mcp::BaseTool
2
+ tool_name "host_stats"
3
+ title "Host resource usage"
4
+ description <<~DESC
5
+ Per host: load average (1/5/15 minutes, and per CPU), CPU count, memory and swap, disk for / and for Docker's data root,
6
+ and uptime. A point-in-time read of /proc and df. No argument reaches the command; hosts and roles only choose which
7
+ configured hosts are asked.
8
+ DESC
9
+ input_schema properties: SCOPE, additionalProperties: false
10
+
11
+ def self.call(server_context:, hosts: nil, roles: nil)
12
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::HostStats.new.to_h }
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ class Dash::Mcp::Tools::LockStatusTool < Dash::Mcp::BaseTool
2
+ tool_name "lock_status"
3
+ title "Deploy lock"
4
+ description "Whether this destination's deploy lock is held, by whom, since when, at which version and with what message."
5
+ input_schema properties: {}, additionalProperties: false
6
+
7
+ def self.call(server_context:)
8
+ answer(server_context) do
9
+ primary = DASH.config.primary_host
10
+ raise ArgumentError, "The deploy lock lives on the primary host #{primary}, outside this server's --hosts" unless DASH.hosts.include?(primary)
11
+
12
+ Dash::Diagnostics::Lock.new(host: primary).to_h
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ class Dash::Mcp::Tools::LogsTool < Dash::Mcp::BaseTool
2
+ tool_name "logs"
3
+ title "Container logs"
4
+ description <<~DESC
5
+ The tail of a role's containers on its hosts (every replica slot), or of an accessory's. Off unless the server was started with --allow-logs
6
+ (or DASH_MCP_ALLOW_LOGS=true): logs can carry personal data. grep is a plain substring match.
7
+ DESC
8
+ input_schema properties: {
9
+ role: { type: "string", description: "The role (default: the primary role)" },
10
+ accessory: { type: "string", description: "An accessory instead of a role (e.g. the database)" },
11
+ hosts: SCOPE[:hosts],
12
+ lines: { type: "integer", minimum: 1, maximum: Dash::Diagnostics::Lines::MAX, description: "Lines per container (default 100)" },
13
+ since: { type: "string", description: "A duration like 15m or 1h30m, or a timestamp like 2026-10-05T10:00:00Z" },
14
+ grep: { type: "string", maxLength: Dash::Diagnostics::Logs::MAX_GREP_LENGTH, description: "Only lines containing this text" }
15
+ }, additionalProperties: false
16
+
17
+ REFUSED = "The logs tool is off: container logs can carry personal data. Restart `dash mcp` with --allow-logs (or DASH_MCP_ALLOW_LOGS=true) to enable it."
18
+
19
+ def self.call(server_context:, role: nil, accessory: nil, hosts: nil, lines: 100, since: nil, grep: nil)
20
+ return error_response(REFUSED) unless server_context.fetch(:session).allow_logs?
21
+ return error_response("Pass a role or an accessory, not both") if role && accessory
22
+
23
+ answer(server_context, hosts: hosts) do
24
+ source = accessory ? { accessory: scoped_accessory(accessory) } : { role: scoped_role(role) }
25
+ Dash::Diagnostics::Logs.new(**source, lines: lines, since: since, grep: grep, redactor: server_context.fetch(:session).redactor).to_h
26
+ end
27
+ end
28
+
29
+ def self.scoped_accessory(name)
30
+ accessory = DASH.config.accessory(name) || raise(ArgumentError, "No accessory #{name} (#{DASH.accessory_names.join(",")})")
31
+ raise ArgumentError, "Accessory #{name} runs on no host within this server's scope" if (accessory.hosts & DASH.accessory_hosts).empty?
32
+
33
+ accessory
34
+ end
35
+
36
+ def self.scoped_role(name)
37
+ return DASH.primary_role unless name
38
+
39
+ DASH.roles.find { |role| role.name == name } || raise(ArgumentError, "No role #{name} within this server's scope (#{DASH.roles.map(&:name).join(",")})")
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ class Dash::Mcp::Tools::PoolMembersTool < Dash::Mcp::BaseTool
2
+ tool_name "pool_members"
3
+ title "Pool members"
4
+ description <<~DESC
5
+ The autoscale pool of every scaled role: its provider, members mode (power or create), host bounds and baseline
6
+ hosts, and per member its provider id, host, state (started, stopped, maintenance, error), labels, whether the
7
+ provider vouched for it (verified false: taken from --hosts while the provider was down), the versions a started
8
+ member runs, and orphan (started but running nothing of the role). A provider that does not answer is the role's error.
9
+ DESC
10
+ input_schema properties: SCOPE, additionalProperties: false
11
+
12
+ def self.call(server_context:, hosts: nil, roles: nil)
13
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::Pool.new.to_h }
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ class Dash::Mcp::Tools::ProxyServicesTool < Dash::Mcp::BaseTool
2
+ tool_name "proxy_services"
3
+ title "Proxy services"
4
+ description "What dash-proxy routes for this deploy on each proxy host (domains, targets, state), and the load balancer's routes when load balancing is on."
5
+ input_schema properties: SCOPE.slice(:hosts), additionalProperties: false
6
+
7
+ def self.call(server_context:, hosts: nil)
8
+ answer(server_context, hosts: hosts) { Dash::Diagnostics::ProxyServices.new.to_h }
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class Dash::Mcp::Tools::ScaleStatusTool < Dash::Mcp::BaseTool
2
+ tool_name "scale_status"
3
+ title "Scale status"
4
+ description "Per role: its replicas bounds (min, max per host), how many containers run in total, and each host's replica slots with version and docker status. Hosts that could not be read are listed under unread. A role with scale also has its host bounds and its pool members (orphan: started but running nothing of the role)."
5
+ input_schema properties: SCOPE, additionalProperties: false
6
+
7
+ def self.call(server_context:, hosts: nil, roles: nil)
8
+ answer(server_context, hosts: hosts, roles: roles) { Dash::Diagnostics::Scale.new.to_h }
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ class Dash::Mcp::Tools::SignalSampleTool < Dash::Mcp::BaseTool
2
+ tool_name "signal_sample"
3
+ title "Signal sample"
4
+ description <<~DESC
5
+ One live sample of a scaled role's autoscale signal (scale.signal: sidekiq or pgbus queue age in seconds, or a Prometheus
6
+ query) and of each of its saturation guards (scale.hold_when), read from the machine dash mcp runs on, each with its latency:
7
+ the source, value, time and error of the signal, where it sits against the role's band (above, inside, below) and the band
8
+ itself (up.above, up.for, down.below, down.for); per guard its index, source, bound, value, error and whether it is holding
9
+ a scale-out. The fields match the signal and guards inputs of the controller's decisions, so a sample compares directly
10
+ with autoscale_explain and autoscale_decisions. Reads only. configured is false for a role without a signal.
11
+ DESC
12
+ input_schema properties: { role: { type: "string", description: "The scaled role" } }, required: [ "role" ], additionalProperties: false
13
+
14
+ def self.call(server_context:, role:)
15
+ answer(server_context) do
16
+ Dash::Diagnostics::AutoscaleSignal.new(role: Dash::Mcp::Tools::AutoscaleExplainTool.scoped_role(role)).to_h
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # stdio, with the protocol on an IO of its own. dash prints from everywhere - SSHKit's
2
+ # formatter, `say`, hooks, warnings - and over stdio any line on stdout is a protocol
3
+ # frame, so the Runner points $stdout at $stderr and hands the real stdout here.
4
+ class Dash::Mcp::Transport < ::MCP::Server::Transports::StdioTransport
5
+ def initialize(server, output:)
6
+ super(server)
7
+ @output = output
8
+ end
9
+
10
+ def send_response(message)
11
+ @output.puts(message.is_a?(String) ? message : JSON.generate(message))
12
+ @output.flush
13
+ end
14
+ end
data/lib/dash/mcp.rb ADDED
@@ -0,0 +1,56 @@
1
+ # `dash mcp`: read-only diagnostics for an AI agent over the Model Context Protocol.
2
+ #
3
+ # Kept out of Zeitwerk (lib/dash.rb ignores lib/dash/mcp/) because the tools subclass
4
+ # `MCP::Tool` from the optional `mcp` gem, which a deploy should never need. This file
5
+ # stays autoloadable so `dash mcp` can say what to install; `load!` pulls in the rest.
6
+ module Dash::Mcp
7
+ module Tools; end
8
+
9
+ REQUIRES = %w[
10
+ dash/mcp/session
11
+ dash/mcp/base_tool
12
+ dash/mcp/tools/config_tool
13
+ dash/mcp/tools/containers_tool
14
+ dash/mcp/tools/proxy_services_tool
15
+ dash/mcp/tools/drift_tool
16
+ dash/mcp/tools/deploy_reports_tool
17
+ dash/mcp/tools/audit_tool
18
+ dash/mcp/tools/lock_status_tool
19
+ dash/mcp/tools/doctor_tool
20
+ dash/mcp/tools/logs_tool
21
+ dash/mcp/tools/scale_status_tool
22
+ dash/mcp/tools/pool_members_tool
23
+ dash/mcp/tools/autoscale_explain_tool
24
+ dash/mcp/tools/autoscale_decisions_tool
25
+ dash/mcp/tools/controller_status_tool
26
+ dash/mcp/tools/signal_sample_tool
27
+ dash/mcp/tools/container_stats_tool
28
+ dash/mcp/tools/host_stats_tool
29
+ dash/mcp/server
30
+ dash/mcp/transport
31
+ dash/mcp/runner
32
+ ].freeze
33
+
34
+ # Everything that prints after this lands on stderr; the returned IO carries only the
35
+ # protocol. `dash mcp` calls it first, before even the gem loads, so nothing - not the
36
+ # "add the gem" error either - can print onto the protocol stream.
37
+ def self.reserve_stdout!
38
+ protocol = $stdout
39
+ protocol.set_encoding("UTF-8")
40
+ $stdout = $stderr
41
+ SSHKit.config.output = SSHKit::Formatter::Pretty.new($stderr)
42
+ protocol
43
+ end
44
+
45
+ # Only the gem's own require is rescued, so a broken dash/mcp/* require shows up as
46
+ # itself instead of as "add the gem".
47
+ def self.load!
48
+ begin
49
+ require "mcp"
50
+ rescue LoadError
51
+ raise Dash::ConfigurationError, "dash mcp needs the `mcp` gem: add `gem \"mcp\", \"~> 1.6\"` to your Gemfile and run `bundle install`"
52
+ end
53
+
54
+ REQUIRES.each { |path| require path }
55
+ end
56
+ end
@@ -45,10 +45,10 @@ class SSHKit::Backend::Abstract
45
45
  $stdout.binmode
46
46
  $stdout.write(output)
47
47
  else
48
- unless quiet
49
- puts "#{type} Host: #{host}"
50
- end
51
- puts "#{output}\n\n"
48
+ # One write: hosts print from parallel threads onto an unbuffered stdout, so a header
49
+ # written on its own can end up above another host's output.
50
+ header = "#{type} Host: #{host}\n" unless quiet
51
+ puts "#{header}#{output}\n\n"
52
52
  end
53
53
  end
54
54
 
data/lib/dash/utils.rb CHANGED
@@ -93,6 +93,13 @@ module Dash::Utils
93
93
  .join
94
94
  end
95
95
 
96
+ # Wraps a value in single quotes, closing and reopening them around any quote the value
97
+ # carries, so the shell reads it as one literal word: no expansion, no command
98
+ # substitution, no way out of the quotes. `grep 'my-id'` stays `grep 'my-id'`.
99
+ def single_quote(value)
100
+ "'#{value.to_s.gsub("'") { %q('\\'') }}'"
101
+ end
102
+
96
103
  def escape_ascii_shell_value(value)
97
104
  value.to_s.dump
98
105
  .gsub(/`/, '\\\\`')
data/lib/dash/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dash
2
- VERSION = "4.1.3"
2
+ VERSION = "4.2.0"
3
3
  end
data/lib/dash.rb CHANGED
@@ -26,5 +26,7 @@ require "digest/sha2"
26
26
 
27
27
  loader = Zeitwerk::Loader.for_gem
28
28
  loader.ignore(File.join(__dir__, "dash", "sshkit_with_ext.rb"))
29
+ # The MCP tools subclass MCP::Tool from the optional `mcp` gem; Dash::Mcp.load! requires them.
30
+ loader.ignore(File.join(__dir__, "dash", "mcp"))
29
31
  loader.setup
30
32
  loader.eager_load_namespace(Dash::Cli) # We need all commands loaded.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dash
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.3
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -177,6 +177,43 @@ files:
177
177
  - README.md
178
178
  - bin/dash
179
179
  - lib/dash.rb
180
+ - lib/dash/autoscale/connections.rb
181
+ - lib/dash/autoscale/connections/builder.rb
182
+ - lib/dash/autoscale/controller.rb
183
+ - lib/dash/autoscale/cron.rb
184
+ - lib/dash/autoscale/decision.rb
185
+ - lib/dash/autoscale/duration.rb
186
+ - lib/dash/autoscale/evaluation.rb
187
+ - lib/dash/autoscale/guard.rb
188
+ - lib/dash/autoscale/lease.rb
189
+ - lib/dash/autoscale/lease_held.rb
190
+ - lib/dash/autoscale/lease_lost.rb
191
+ - lib/dash/autoscale/member.rb
192
+ - lib/dash/autoscale/metrics.rb
193
+ - lib/dash/autoscale/metrics_server.rb
194
+ - lib/dash/autoscale/observation.rb
195
+ - lib/dash/autoscale/pause.rb
196
+ - lib/dash/autoscale/policy.rb
197
+ - lib/dash/autoscale/pool.rb
198
+ - lib/dash/autoscale/prometheus_client.rb
199
+ - lib/dash/autoscale/provider.rb
200
+ - lib/dash/autoscale/provider/base.rb
201
+ - lib/dash/autoscale/provider/exec.rb
202
+ - lib/dash/autoscale/provider/upcloud.rb
203
+ - lib/dash/autoscale/provider_error.rb
204
+ - lib/dash/autoscale/read_only_query.rb
205
+ - lib/dash/autoscale/reader.rb
206
+ - lib/dash/autoscale/recorder.rb
207
+ - lib/dash/autoscale/role_state.rb
208
+ - lib/dash/autoscale/sample.rb
209
+ - lib/dash/autoscale/signal.rb
210
+ - lib/dash/autoscale/signal/pgbus.rb
211
+ - lib/dash/autoscale/signal/prometheus.rb
212
+ - lib/dash/autoscale/signal/sidekiq.rb
213
+ - lib/dash/autoscale/simulation.rb
214
+ - lib/dash/autoscale/state_store.rb
215
+ - lib/dash/autoscale/timestamp.rb
216
+ - lib/dash/autoscale/tunnel.rb
180
217
  - lib/dash/build/progress_parser.rb
181
218
  - lib/dash/build/report.rb
182
219
  - lib/dash/build/step.rb
@@ -186,17 +223,17 @@ files:
186
223
  - lib/dash/cli/app.rb
187
224
  - lib/dash/cli/app/assets.rb
188
225
  - lib/dash/cli/app/boot.rb
226
+ - lib/dash/cli/app/boot_state.rb
189
227
  - lib/dash/cli/app/error_pages.rb
190
228
  - lib/dash/cli/app/rollout_boot.rb
229
+ - lib/dash/cli/app/running_targets.rb
230
+ - lib/dash/cli/app/slot_version.rb
191
231
  - lib/dash/cli/app/ssl_certificates.rb
232
+ - lib/dash/cli/autoscale.rb
192
233
  - lib/dash/cli/base.rb
193
234
  - lib/dash/cli/build.rb
194
235
  - lib/dash/cli/build/clone.rb
195
236
  - lib/dash/cli/build/port_forwarding.rb
196
- - lib/dash/cli/doctor.rb
197
- - lib/dash/cli/doctor/config_checks.rb
198
- - lib/dash/cli/doctor/endpoint_checks.rb
199
- - lib/dash/cli/doctor/host_checks.rb
200
237
  - lib/dash/cli/healthcheck/barrier.rb
201
238
  - lib/dash/cli/healthcheck/drift_error.rb
202
239
  - lib/dash/cli/healthcheck/error.rb
@@ -207,15 +244,26 @@ files:
207
244
  - lib/dash/cli/main/migrate.rb
208
245
  - lib/dash/cli/proxy.rb
209
246
  - lib/dash/cli/proxy/drift.rb
247
+ - lib/dash/cli/proxy/inspection.rb
210
248
  - lib/dash/cli/proxy/legacy_rename.rb
211
249
  - lib/dash/cli/proxy/loadbalancer_claim.rb
250
+ - lib/dash/cli/proxy/loadbalancer_deploy.rb
212
251
  - lib/dash/cli/proxy/loadbalancer_reboot.rb
213
252
  - lib/dash/cli/proxy/reboot.rb
214
253
  - lib/dash/cli/prune.rb
215
254
  - lib/dash/cli/registry.rb
216
255
  - lib/dash/cli/report.rb
256
+ - lib/dash/cli/scale.rb
257
+ - lib/dash/cli/scale/host_plan.rb
258
+ - lib/dash/cli/scale/join.rb
259
+ - lib/dash/cli/scale/leave.rb
260
+ - lib/dash/cli/scale/placement.rb
261
+ - lib/dash/cli/scale/power_off.rb
262
+ - lib/dash/cli/scale/replica_join.rb
263
+ - lib/dash/cli/scale/replica_leave.rb
217
264
  - lib/dash/cli/secrets.rb
218
265
  - lib/dash/cli/server.rb
266
+ - lib/dash/cli/stats_table.rb
219
267
  - lib/dash/cli/templates/deploy.yml
220
268
  - lib/dash/cli/templates/sample_hooks/docker-setup.sample
221
269
  - lib/dash/cli/templates/sample_hooks/post-app-boot.sample
@@ -223,6 +271,8 @@ files:
223
271
  - lib/dash/cli/templates/sample_hooks/post-deploy.sample
224
272
  - lib/dash/cli/templates/sample_hooks/post-proxy-deploy.sample
225
273
  - lib/dash/cli/templates/sample_hooks/post-proxy-reboot.sample
274
+ - lib/dash/cli/templates/sample_hooks/post-scale-in.sample
275
+ - lib/dash/cli/templates/sample_hooks/post-scale-out.sample
226
276
  - lib/dash/cli/templates/sample_hooks/pre-app-boot.sample
227
277
  - lib/dash/cli/templates/sample_hooks/pre-app-stop.sample
228
278
  - lib/dash/cli/templates/sample_hooks/pre-build.sample
@@ -230,6 +280,8 @@ files:
230
280
  - lib/dash/cli/templates/sample_hooks/pre-deploy.sample
231
281
  - lib/dash/cli/templates/sample_hooks/pre-proxy-deploy.sample
232
282
  - lib/dash/cli/templates/sample_hooks/pre-proxy-reboot.sample
283
+ - lib/dash/cli/templates/sample_hooks/pre-scale-in.sample
284
+ - lib/dash/cli/templates/sample_hooks/pre-scale-out.sample
233
285
  - lib/dash/cli/templates/secrets
234
286
  - lib/dash/commander.rb
235
287
  - lib/dash/commander/specifics.rb
@@ -245,6 +297,7 @@ files:
245
297
  - lib/dash/commands/app/logging.rb
246
298
  - lib/dash/commands/app/proxy.rb
247
299
  - lib/dash/commands/auditor.rb
300
+ - lib/dash/commands/autoscale.rb
248
301
  - lib/dash/commands/base.rb
249
302
  - lib/dash/commands/builder.rb
250
303
  - lib/dash/commands/builder/base.rb
@@ -260,6 +313,7 @@ files:
260
313
  - lib/dash/commands/lock.rb
261
314
  - lib/dash/commands/proxy.rb
262
315
  - lib/dash/commands/proxy/cert_transfer.rb
316
+ - lib/dash/commands/proxy/services.rb
263
317
  - lib/dash/commands/proxy/state.rb
264
318
  - lib/dash/commands/prune.rb
265
319
  - lib/dash/commands/registry.rb
@@ -267,10 +321,12 @@ files:
267
321
  - lib/dash/configuration.rb
268
322
  - lib/dash/configuration/accessory.rb
269
323
  - lib/dash/configuration/alias.rb
324
+ - lib/dash/configuration/autoscale.rb
270
325
  - lib/dash/configuration/boot.rb
271
326
  - lib/dash/configuration/builder.rb
272
327
  - lib/dash/configuration/docs/accessory.yml
273
328
  - lib/dash/configuration/docs/alias.yml
329
+ - lib/dash/configuration/docs/autoscale.yml
274
330
  - lib/dash/configuration/docs/boot.yml
275
331
  - lib/dash/configuration/docs/builder.yml
276
332
  - lib/dash/configuration/docs/configuration.yml
@@ -296,7 +352,13 @@ files:
296
352
  - lib/dash/configuration/registry.rb
297
353
  - lib/dash/configuration/report.rb
298
354
  - lib/dash/configuration/role.rb
355
+ - lib/dash/configuration/role/drain.rb
299
356
  - lib/dash/configuration/role/healthcheck.rb
357
+ - lib/dash/configuration/role/replicas.rb
358
+ - lib/dash/configuration/role/scale.rb
359
+ - lib/dash/configuration/role/scale/guard.rb
360
+ - lib/dash/configuration/role/scale/signal.rb
361
+ - lib/dash/configuration/role/scale/window.rb
300
362
  - lib/dash/configuration/servers.rb
301
363
  - lib/dash/configuration/ssh.rb
302
364
  - lib/dash/configuration/sshkit.rb
@@ -304,6 +366,7 @@ files:
304
366
  - lib/dash/configuration/validator.rb
305
367
  - lib/dash/configuration/validator/accessory.rb
306
368
  - lib/dash/configuration/validator/alias.rb
369
+ - lib/dash/configuration/validator/autoscale.rb
307
370
  - lib/dash/configuration/validator/builder.rb
308
371
  - lib/dash/configuration/validator/configuration.rb
309
372
  - lib/dash/configuration/validator/env.rb
@@ -312,6 +375,34 @@ files:
312
375
  - lib/dash/configuration/validator/role.rb
313
376
  - lib/dash/configuration/validator/servers.rb
314
377
  - lib/dash/configuration/volume.rb
378
+ - lib/dash/diagnostics/audit.rb
379
+ - lib/dash/diagnostics/autoscale_decisions.rb
380
+ - lib/dash/diagnostics/autoscale_explain.rb
381
+ - lib/dash/diagnostics/autoscale_signal.rb
382
+ - lib/dash/diagnostics/base.rb
383
+ - lib/dash/diagnostics/config.rb
384
+ - lib/dash/diagnostics/container_stats.rb
385
+ - lib/dash/diagnostics/containers.rb
386
+ - lib/dash/diagnostics/controller_status.rb
387
+ - lib/dash/diagnostics/docker_ps.rb
388
+ - lib/dash/diagnostics/doctor.rb
389
+ - lib/dash/diagnostics/doctor/config_checks.rb
390
+ - lib/dash/diagnostics/doctor/controller_checks.rb
391
+ - lib/dash/diagnostics/doctor/endpoint_checks.rb
392
+ - lib/dash/diagnostics/doctor/host_checks.rb
393
+ - lib/dash/diagnostics/doctor/pool_checks.rb
394
+ - lib/dash/diagnostics/doctor/signal_checks.rb
395
+ - lib/dash/diagnostics/drift.rb
396
+ - lib/dash/diagnostics/host_stats.rb
397
+ - lib/dash/diagnostics/lines.rb
398
+ - lib/dash/diagnostics/lock.rb
399
+ - lib/dash/diagnostics/logs.rb
400
+ - lib/dash/diagnostics/pool.rb
401
+ - lib/dash/diagnostics/proxy_services.rb
402
+ - lib/dash/diagnostics/redactor.rb
403
+ - lib/dash/diagnostics/reports.rb
404
+ - lib/dash/diagnostics/scale.rb
405
+ - lib/dash/diagnostics/units.rb
315
406
  - lib/dash/docker.rb
316
407
  - lib/dash/dockerfile/analyzer.rb
317
408
  - lib/dash/dockerfile/context.rb
@@ -340,6 +431,29 @@ files:
340
431
  - lib/dash/dockerfile/stage.rb
341
432
  - lib/dash/env_file.rb
342
433
  - lib/dash/git.rb
434
+ - lib/dash/mcp.rb
435
+ - lib/dash/mcp/base_tool.rb
436
+ - lib/dash/mcp/runner.rb
437
+ - lib/dash/mcp/server.rb
438
+ - lib/dash/mcp/session.rb
439
+ - lib/dash/mcp/tools/audit_tool.rb
440
+ - lib/dash/mcp/tools/autoscale_decisions_tool.rb
441
+ - lib/dash/mcp/tools/autoscale_explain_tool.rb
442
+ - lib/dash/mcp/tools/config_tool.rb
443
+ - lib/dash/mcp/tools/container_stats_tool.rb
444
+ - lib/dash/mcp/tools/containers_tool.rb
445
+ - lib/dash/mcp/tools/controller_status_tool.rb
446
+ - lib/dash/mcp/tools/deploy_reports_tool.rb
447
+ - lib/dash/mcp/tools/doctor_tool.rb
448
+ - lib/dash/mcp/tools/drift_tool.rb
449
+ - lib/dash/mcp/tools/host_stats_tool.rb
450
+ - lib/dash/mcp/tools/lock_status_tool.rb
451
+ - lib/dash/mcp/tools/logs_tool.rb
452
+ - lib/dash/mcp/tools/pool_members_tool.rb
453
+ - lib/dash/mcp/tools/proxy_services_tool.rb
454
+ - lib/dash/mcp/tools/scale_status_tool.rb
455
+ - lib/dash/mcp/tools/signal_sample_tool.rb
456
+ - lib/dash/mcp/transport.rb
343
457
  - lib/dash/otel_shipper.rb
344
458
  - lib/dash/output/base_logger.rb
345
459
  - lib/dash/output/file_logger.rb