How to Add Real‑Time User Monitoring and Forced Disconnect to Django WebSSH
This article explains how to extend a Django Channels‑based WebSSH tool with real‑time operation monitoring, group‑based messaging, and a forced‑disconnect feature, detailing the necessary layer configuration, consumer modifications, and WebSocket message handling for both operators and observers.
这个功能我可以不用,但你不能没有。
Previous articles implemented WebSSH operations for physical machines, virtual machines, and Kubernetes Pods, supporting full‑session recording for later review and audit.
This article adds a seemingly flashy but essential feature: real‑time monitoring of user actions and the ability to kick users offline when needed.
Real‑time Operation View
Django Channels uses a concept called a layer to combine multiple channels into a group, allowing messages sent to the group to be received by every channel within it.
For background on Channels, see the earlier articles on implementing a chatroom and a web‑based tail‑f feature.
The original WebSSH used a single connection without a layer. To enable monitoring, we must merge the operator’s and the monitor’s channels into a group so that all operator actions are broadcast to the monitor in real time. The flow change is illustrated below:
Implementation steps (based on the previous article):
Enable the layer in
settings.py:
<code>CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('ops-coffee.cn', 6379)],
},
},
}</code>Modify the existing
SSHConsumerto support the layer. Key changes include creating a
group_nameduring connection, adding the channel to the group, and recording connection details.
<code>class SSHConsumer(WebsocketConsumer):
def connect(self):
ssh_connect_args = args(self.scope)
self.host = Host.objects.get(host=ssh_connect_args.get('host'))
self.group_name = '%s-%s-%d' % (
ssh_connect_args.get('host'), ssh_connect_args.get('username'), time.time())
self.therecord = Record.objects.create(
host=self.host,
user=self.scope['user'],
group=self.group_name,
channel=self.channel_name,
cols=ssh_connect_args.get('cols'),
rows=ssh_connect_args.get('rows'),
is_connecting=True
)
async_to_sync(self.channel_layer.group_add)(
self.group_name,
self.channel_name
)
self.accept()
self.ssh = SSHBridge(self.therecord, websocket=self)
self.ssh.connect(**ssh_connect_args)
def disconnect(self, close_code):
self.therecord.is_connecting = False
self.therecord.save()
async_to_sync(self.channel_layer.group_discard)(
self.group_name,
self.channel_name
)
self.ssh.close()
def receive(self, text_data=None):
text_data = json.loads(text_data)
if text_data.get('flag') == 'resize':
self.ssh.resize_pty(cols=text_data['cols'], rows=text_data['rows'])
else:
self.ssh.shell(data=text_data.get('data', ''))
def ssh_message(self, event):
self.send(text_data=json.dumps(event['message']))
</code>During connection, a record stores host, user,
group_name,
channel_name, and initial terminal size, marking
is_connectingas True. The
group_namefollows the same naming rule as the recording file defined in the earlier WebSSH recording article.
When the connection closes,
is_connectingis set to False, allowing the front‑end to toggle between monitoring/force‑stop buttons and playback/extract‑command buttons.
Next, create a
MonitorConsumerto handle monitoring connections. It joins the same group as the operator and only receives messages without sending any.
<code>class MonitorConsumer(WebsocketConsumer):
def connect(self):
pk = self.scope['url_route']['kwargs'].get('id')
self.group_name = Record.objects.get(id=pk).group
async_to_sync(self.channel_layer.group_add)(
self.group_name,
self.channel_name
)
self.accept()
self.connecting = Record.objects.get(id=pk).is_connecting
if not self.connecting:
self.close()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.group_name,
self.channel_name
)
self.close()
def receive(self, text_data=None):
pass
def ssh_message(self, event):
self.send(text_data=json.dumps(event['message']))
</code>The differences from
SSHConsumerare: the group name is retrieved from the existing record, and the monitor does not send terminal data back to the server.
Finally, adjust
SSHBridgeto broadcast messages to the group instead of a single WebSocket:
<code>async_to_sync(self.websocket.channel_layer.group_send)(
self.group_name,
{
'type': 'ssh.message',
'message': message
}
)</code>With this change, all channels in the group receive the same output, enabling real‑time monitoring.
Kick User Offline
To force‑stop a user, the front‑end sends a request with the record ID to a view, which retrieves the corresponding
group_nameand sends a
disconnectmessage to the group. All channels in that group then close.
<code>from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
async_to_sync(get_channel_layer().group_send)(
Record.objects.get(id=pk).group,
{'type': 'disconnect'}
)</code>Demo and Explanation
All components interlock; reading the entire series will give you a solid grasp of WebSockets and Django Channels, enabling you to build a simple bastion host with powerful monitoring capabilities.
The original goal was to add WebSSH to the Alodi system for quick debugging during development, but it evolved into a feature‑rich series exploring many interesting aspects of real‑time communication.
Efficient Ops
This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.