]>
git.proxmox.com Git - pve-ha-manager.git/blob - src/PVE/HA/Sim/RTHardware.pm
1 package PVE
::HA
::Sim
::RTHardware
;
3 # Simulate Hardware resources in Realtime by
4 # running CRM and LRM in separate processes
8 use POSIX
qw(strftime EINTR);
10 $Data::Dumper
::Sortkeys
= 1; # fixes 'random' output behaviour of manager status
14 use Fcntl
qw(:DEFAULT :flock);
16 use File
::Path
qw(make_path remove_tree);
25 use PVE
::HA
::Sim
::RTEnv
;
26 use base
qw(PVE::HA::Sim::Hardware);
29 my ($this, $testdir) = @_;
31 my $class = ref($this) || $this;
33 my $self = $class->SUPER::new
($testdir);
35 my $logfile = "$testdir/log";
36 $self->{logfh
} = IO
::File-
>new(">$logfile") ||
37 die "unable to open '$logfile' - $!";
39 foreach my $node (sort keys %{$self->{nodes
}}) {
40 my $d = $self->{nodes
}->{$node};
42 $d->{crm
} = undef; # create on power on
43 $d->{lrm
} = undef; # create on power on
46 $self->create_main_window();
58 my ($self, $level, $msg, $id) = @_;
62 my $time = $self->get_time();
64 $id = 'hardware' if !$id;
66 my $text = sprintf("%-5s %10s %12s: $msg\n", $level,
67 strftime
("%H:%M:%S", localtime($time)), $id);
69 $self->append_text($text);
72 # fixme: duplicate code in Env?
73 sub read_manager_status
{
76 my $filename = "$self->{statusdir}/manager_status";
78 return PVE
::HA
::Tools
::read_json_from_file
($filename, {});
82 my ($self, $lockfh, $type, $node) = @_;
84 my @psync = POSIX
::pipe();
87 die "fork failed" if ! defined($pid);
91 close($lockfh) if defined($lockfh); # unlock global lock
93 POSIX
::close($psync[0]);
95 my $outfh = $psync[1];
97 my $fd = fileno (STDIN
);
99 POSIX
::close(0) if $fd != 0;
101 die "unable to redirect STDIN - $!"
102 if !open(STDIN
, "</dev/null");
105 $fd = fileno(STDOUT
);
107 POSIX
::close (1) if $fd != 1;
109 die "unable to redirect STDOUT - $!"
110 if !open(STDOUT
, ">&", $outfh);
112 STDOUT-
>autoflush (1);
114 # redirect STDERR to STDOUT
115 $fd = fileno(STDERR
);
117 POSIX
::close(2) if $fd != 2;
119 die "unable to redirect STDERR - $!"
120 if !open(STDERR
, ">&1");
122 STDERR-
>autoflush(1);
124 if ($type eq 'crm') {
126 my $haenv = PVE
::HA
::Env-
>new('PVE::HA::Sim::RTEnv', $node, $self, 'crm');
128 my $crm = PVE
::HA
::CRM-
>new($haenv);
131 $haenv->loop_start_hook();
133 if (!$crm->do_one_iteration()) {
134 $haenv->log("info", "daemon stopped");
138 $haenv->loop_end_hook();
143 my $haenv = PVE
::HA
::Env-
>new('PVE::HA::Sim::RTEnv', $node, $self, 'lrm');
145 my $lrm = PVE
::HA
::LRM-
>new($haenv);
148 $haenv->loop_start_hook();
150 if (!$lrm->do_one_iteration()) {
151 $haenv->log("info", "daemon stopped");
155 $haenv->loop_end_hook();
164 POSIX
::close ($psync[1]);
166 Glib
::IO-
>add_watch($psync[0], ['in', 'hup'], sub {
167 my ($fd, $cond) = @_;
170 if (my $count = POSIX
::read($fd, $readbuf, 8192)) {
171 $self->append_text($readbuf);
183 # simulate hardware commands
184 # power <node> <on|off>
185 # network <node> <on|off>
187 sub sim_hardware_cmd
{
188 my ($self, $cmdstr, $logid) = @_;
192 # note: do not fork when we own the lock!
196 $cstatus = $self->read_hardware_status_nolock();
198 my ($cmd, $node, $action) = split(/\s+/, $cmdstr);
200 die "sim_hardware_cmd: no node specified" if !$node;
201 die "sim_hardware_cmd: unknown action '$action'" if $action !~ m/^(on|off)$/;
203 my $d = $self->{nodes
}->{$node};
204 die "sim_hardware_cmd: no such node '$node'\n" if !$d;
206 $self->log('info', "execute $cmdstr", $logid);
208 if ($cmd eq 'power') {
209 if ($cstatus->{$node}->{power
} ne $action) {
210 if ($action eq 'on') {
211 $d->{crm
} = $self->fork_daemon($lockfh, 'crm', $node) if !$d->{crm
};
212 $d->{lrm
} = $self->fork_daemon($lockfh, 'lrm', $node) if !$d->{lrm
};
215 $self->log('info', "crm on node '$node' killed by poweroff");
220 $self->log('info', "lrm on node '$node' killed by poweroff");
224 $self->watchdog_reset_nolock($node);
225 $self->write_service_status($node, {});
229 $cstatus->{$node}->{power
} = $action;
230 $cstatus->{$node}->{network
} = $action;
232 } elsif ($cmd eq 'network') {
233 $cstatus->{$node}->{network
} = $action;
235 die "sim_hardware_cmd: unknown command '$cmd'\n";
238 $self->write_hardware_status_nolock($cstatus);
241 my $res = $self->global_lock($code);
243 # update GUI outside lock
245 foreach my $node (keys %$cstatus) {
246 my $d = $self->{nodes
}->{$node};
247 $d->{network_btn
}->set_active($cstatus->{$node}->{network
} eq 'on');
248 $d->{power_btn
}->set_active($cstatus->{$node}->{power
} eq 'on');
257 my @nodes = sort keys %{$self->{nodes
}};
258 foreach my $node (@nodes) {
259 my $d = $self->{nodes
}->{$node};
273 my ($self, $text) = @_;
275 $self->{logfh
}->print($text);
276 $self->{logfh
}->flush();
278 my $logview = $self->{gui
}->{text_view
} || die "GUI not ready";
279 my $textbuf = $logview->get_buffer();
281 $textbuf->insert_at_cursor($text, -1);
282 my $lines = $textbuf->get_line_count();
286 if ($lines > $history) {
287 my $start = $textbuf->get_iter_at_line(0);
288 my $end = $textbuf->get_iter_at_line($lines - $history);
289 $textbuf->delete($start, $end);
292 $logview->scroll_to_mark($textbuf->get_insert(), 0.0, 1, 0.0, 1.0);
295 sub set_power_state
{
296 my ($self, $node) = @_;
298 my $d = $self->{nodes
}->{$node} || die "no such node '$node'";
300 my $action = $d->{power_btn
}->get_active() ?
'on' : 'off';
302 $self->sim_hardware_cmd("power $node $action");
305 sub set_network_state
{
306 my ($self, $node) = @_;
308 my $d = $self->{nodes
}->{$node} || die "no such node '$node'";
310 my $action = $d->{network_btn
}->get_active() ?
'on' : 'off';
312 $self->sim_hardware_cmd("network $node $action");
315 sub set_service_state
{
316 my ($self, $sid) = @_;
318 my $d = $self->{service_gui
}->{$sid} || die "no such service '$sid'";
319 my $state = $d->{enable_btn
}->get_active() ?
'enabled' : 'disabled';
321 $self->{service_config
} = $self->SUPER::set_service_state
($sid, $state);
325 sub create_node_control
{
328 my $ngrid = Gtk3
::Grid-
>new();
329 $ngrid->set_row_spacing(2);
330 $ngrid->set_column_spacing(5);
331 $ngrid->set('margin-left', 5);
333 my $w = Gtk3
::Label-
>new('Node');
334 $ngrid->attach($w, 0, 0, 1, 1);
335 $w = Gtk3
::Label-
>new('Power');
336 $ngrid->attach($w, 1, 0, 1, 1);
337 $w = Gtk3
::Label-
>new('Network');
338 $ngrid->attach($w, 2, 0, 1, 1);
339 $w = Gtk3
::Label-
>new('Status');
340 $w->set_size_request(150, -1);
341 $w->set_alignment (0, 0.5);
342 $ngrid->attach($w, 3, 0, 1, 1);
346 my @nodes = sort keys %{$self->{nodes
}};
348 foreach my $node (@nodes) {
349 my $d = $self->{nodes
}->{$node};
351 $w = Gtk3
::Label-
>new($node);
352 $ngrid->attach($w, 0, $row, 1, 1);
353 $w = Gtk3
::Switch-
>new();
354 $ngrid->attach($w, 1, $row, 1, 1);
355 $d->{power_btn
} = $w;
356 $w->signal_connect('notify::active' => sub {
357 $self->set_power_state($node);
360 $w = Gtk3
::Switch-
>new();
361 $ngrid->attach($w, 2, $row, 1, 1);
362 $d->{network_btn
} = $w;
363 $w->signal_connect('notify::active' => sub {
364 $self->set_network_state($node);
367 $w = Gtk3
::Label-
>new('-');
368 $w->set_alignment (0, 0.5);
369 $ngrid->attach($w, 3, $row, 1, 1);
370 $d->{node_status_label
} = $w;
378 sub show_migrate_dialog
{
379 my ($self, $sid) = @_;
381 my $dialog = Gtk3
::Dialog-
>new();
383 $dialog->set_title("Migrate $sid");
384 $dialog->set_modal(1);
386 my $grid = Gtk3
::Grid-
>new();
387 $grid->set_row_spacing(2);
388 $grid->set_column_spacing(5);
389 $grid->set('margin', 5);
391 my $w = Gtk3
::Label-
>new('Target Mode');
392 $grid->attach($w, 0, 0, 1, 1);
394 my @nodes = sort keys %{$self->{nodes
}};
395 $w = Gtk3
::ComboBoxText-
>new();
396 foreach my $node (@nodes) {
397 $w->append_text($node);
401 $w->signal_connect('notify::active' => sub {
404 my $sel = $w->get_active();
407 $target = $nodes[$sel];
409 $grid->attach($w, 1, 0, 1, 1);
411 my $relocate_btn = Gtk3
::CheckButton-
>new_with_label("stop service (relocate)");
412 $grid->attach($relocate_btn, 1, 1, 1, 1);
414 my $contarea = $dialog->get_content_area();
416 $contarea->add($grid);
418 $dialog->add_button("_OK", 1);
421 my $res = $dialog->run();
425 if ($res == 1 && $target) {
426 if ($relocate_btn->get_active()) {
427 $self->queue_crm_commands("relocate $sid $target");
429 $self->queue_crm_commands("migrate $sid $target");
434 sub create_service_control
{
437 my $sgrid = Gtk3
::Grid-
>new();
438 $sgrid->set_row_spacing(2);
439 $sgrid->set_column_spacing(5);
440 $sgrid->set('margin', 5);
442 my $w = Gtk3
::Label-
>new('Service');
443 $sgrid->attach($w, 0, 0, 1, 1);
444 $w = Gtk3
::Label-
>new('Enable');
445 $sgrid->attach($w, 1, 0, 1, 1);
446 $w = Gtk3
::Label-
>new('Node');
447 $sgrid->attach($w, 3, 0, 1, 1);
448 $w = Gtk3
::Label-
>new('Status');
449 $w->set_alignment (0, 0.5);
450 $w->set_size_request(150, -1);
451 $sgrid->attach($w, 4, 0, 1, 1);
454 my @nodes = keys %{$self->{nodes
}};
456 foreach my $sid (sort keys %{$self->{service_config
}}) {
457 my $d = $self->{service_config
}->{$sid};
459 $w = Gtk3
::Label-
>new($sid);
460 $sgrid->attach($w, 0, $row, 1, 1);
462 $w = Gtk3
::Switch-
>new();
463 $sgrid->attach($w, 1, $row, 1, 1);
464 $w->set_active(1) if $d->{state} eq 'enabled';
465 $self->{service_gui
}->{$sid}->{enable_btn
} = $w;
466 $w->signal_connect('notify::active' => sub {
467 $self->set_service_state($sid);
471 $w = Gtk3
::Button-
>new('Migrate');
472 $sgrid->attach($w, 2, $row, 1, 1);
473 $w->signal_connect(clicked
=> sub {
474 $self->show_migrate_dialog($sid);
477 $w = Gtk3
::Label-
>new($d->{node
});
478 $sgrid->attach($w, 3, $row, 1, 1);
479 $self->{service_gui
}->{$sid}->{node_label
} = $w;
481 $w = Gtk3
::Label-
>new('-');
482 $w->set_alignment (0, 0.5);
483 $sgrid->attach($w, 4, $row, 1, 1);
484 $self->{service_gui
}->{$sid}->{status_label
} = $w;
492 sub create_log_view
{
495 my $nb = Gtk3
::Notebook-
>new();
497 my $l1 = Gtk3
::Label-
>new('Cluster Log');
499 my $logview = Gtk3
::TextView-
>new();
500 $logview->set_editable(0);
501 $logview->set_cursor_visible(0);
503 $self->{gui
}->{text_view
} = $logview;
505 my $swindow = Gtk3
::ScrolledWindow-
>new();
506 $swindow->set_size_request(1024, 768);
507 $swindow->add($logview);
509 $nb->insert_page($swindow, $l1, 0);
511 my $l2 = Gtk3
::Label-
>new('Manager Status');
513 my $statview = Gtk3
::TextView-
>new();
514 $statview->set_editable(0);
515 $statview->set_cursor_visible(0);
517 $self->{gui
}->{stat_view
} = $statview;
519 $swindow = Gtk3
::ScrolledWindow-
>new();
520 $swindow->set_size_request(640, 400);
521 $swindow->add($statview);
523 $nb->insert_page($swindow, $l2, 1);
527 sub create_main_window
{
530 my $window = Gtk3
::Window-
>new();
531 $window->set_title("Proxmox HA Simulator");
533 $window->signal_connect( destroy
=> sub { Gtk3
::main_quit
(); });
535 my $grid = Gtk3
::Grid-
>new();
537 my $frame = $self->create_log_view();
538 $grid->attach($frame, 0, 0, 1, 1);
539 $frame->set('expand', 1);
541 my $vbox = Gtk3
::VBox-
>new(0, 0);
542 $grid->attach($vbox, 1, 0, 1, 1);
544 my $ngrid = $self->create_node_control();
545 $vbox->pack_start($ngrid, 0, 0, 0);
547 my $sep = Gtk3
::HSeparator-
>new;
548 $sep->set('margin-top', 10);
549 $vbox->pack_start ($sep, 0, 0, 0);
551 my $sgrid = $self->create_service_control();
552 $vbox->pack_start($sgrid, 0, 0, 0);
563 Glib
::Timeout-
>add(1000, sub {
565 $self->{service_config
} = $self->read_service_config();
567 # check all watchdogs
568 my @nodes = sort keys %{$self->{nodes
}};
569 foreach my $node (@nodes) {
570 if (!$self->watchdog_check($node)) {
571 $self->sim_hardware_cmd("power $node off", 'watchdog');
572 $self->log('info', "server '$node' stopped by poweroff (watchdog)");
576 my $mstatus = $self->read_manager_status();
577 my $node_status = $mstatus->{node_status
} || {};
579 foreach my $node (@nodes) {
580 my $ns = $node_status->{$node} || '-';
581 my $d = $self->{nodes
}->{$node};
583 my $sl = $d->{node_status_label
};
586 if ($mstatus->{master_node
} && ($mstatus->{master_node
} eq $node)) {
587 $sl->set_text(uc($ns));
593 my $service_status = $mstatus->{service_status
} || {};
594 my @services = sort keys %{$self->{service_config
}};
596 foreach my $sid (@services) {
597 my $sc = $self->{service_config
}->{$sid};
598 my $ss = $service_status->{$sid};
599 my $sgui = $self->{service_gui
}->{$sid};
601 my $nl = $sgui->{node_label
};
602 $nl->set_text($sc->{node
});
604 my $sl = $sgui->{status_label
};
607 my $text = ($ss && $ss->{state}) ?
$ss->{state} : '-';
608 $sl->set_text($text);
611 if (my $sv = $self->{gui
}->{stat_view
}) {
612 my $text = Dumper
($mstatus);
613 my $textbuf = $sv->get_buffer();
614 $textbuf->set_text($text, -1);