]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXC/Migrate.pm
migration: fail when aliased volume is detected
[pve-container.git] / src / PVE / LXC / Migrate.pm
CommitLineData
6f42807e
DM
1package PVE::LXC::Migrate;
2
3use strict;
4use warnings;
88685441 5
6f42807e
DM
6use File::Basename;
7use File::Copy; # fixme: remove
88685441 8
6f42807e 9use PVE::Cluster;
88685441
TL
10use PVE::INotify;
11use PVE::Replication;
12use PVE::ReplicationConfig;
13use PVE::ReplicationState;
6f42807e 14use PVE::Storage;
88685441
TL
15use PVE::Tools;
16
8d66edee 17use PVE::LXC::Config;
6f42807e
DM
18use PVE::LXC;
19
88685441 20use PVE::AbstractMigrate;
6f42807e
DM
21use base qw(PVE::AbstractMigrate);
22
0e0958a5
FG
23# compared against remote end's minimum version
24our $WS_TUNNEL_VERSION = 2;
25
6f42807e
DM
26sub lock_vm {
27 my ($self, $vmid, $code, @param) = @_;
28
67afe46e 29 return PVE::LXC::Config->lock_config($vmid, $code, @param);
6f42807e
DM
30}
31
32sub prepare {
33 my ($self, $vmid) = @_;
34
35 my $online = $self->{opts}->{online};
a7cedb73 36 my $restart= $self->{opts}->{restart};
0e0958a5 37 my $remote = $self->{opts}->{remote};
6f42807e
DM
38
39 $self->{storecfg} = PVE::Storage::config();
40
1cd1fa12 41 # test if CT exists
67afe46e 42 my $conf = $self->{vmconf} = PVE::LXC::Config->load_config($vmid);
6f42807e 43
67afe46e 44 PVE::LXC::Config->check_lock($conf);
6f42807e
DM
45
46 my $running = 0;
47 if (PVE::LXC::check_running($vmid)) {
a7cedb73
DC
48 die "lxc live migration is currently not implemented\n" if $online;
49 die "running container can only be migrated in restart mode" if !$restart;
6f42807e
DM
50 $running = 1;
51 }
a7cedb73 52 $self->{was_running} = $running;
6f42807e 53
0e0958a5 54 my $storages = {};
2334b15d 55 PVE::LXC::Config->foreach_volume_full($conf, { include_unused => 1 }, sub {
6f42807e
DM
56 my ($ms, $mountpoint) = @_;
57
552e168f 58 my $type = $mountpoint->{type};
154b1295 59 # skip dev/bind mps when shared
552e168f 60 if ($type ne 'volume') {
552e168f
FG
61 if ($mountpoint->{shared}) {
62 return;
63 } else {
64 die "cannot migrate local $type mount point '$ms'\n";
65 }
9746c095 66 }
552e168f 67
e3b12100
TL
68 my $volid = $mountpoint->{volume} or die "missing volume for mount point '$ms'\n";
69
70 my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
235dbdf3 71 die "can't determine assigned storage for mount point '$ms'\n" if !$storage;
6f42807e
DM
72
73 # check if storage is available on both nodes
a1c27f86 74 my $scfg = PVE::Storage::storage_check_enabled($self->{storecfg}, $storage);
e90ddc4c
FG
75
76 my $targetsid = $storage;
6f42807e 77
b2f00a89
FE
78 die "content type 'rootdir' is not available on storage '$storage'\n"
79 if !$scfg->{content}->{rootdir};
20ab40f3 80
0e0958a5 81 if ($scfg->{shared} && !$remote) {
20ab40f3
FG
82 # PVE::Storage::activate_storage checks this for non-shared storages
83 my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
84 warn "Used shared storage '$storage' is not online on source node!\n"
85 if !$plugin->check_connection($storage, $scfg);
86 } else {
a7cedb73 87 # unless in restart mode because we shut the container down
235dbdf3 88 die "unable to migrate local mount point '$volid' while CT is running"
a7cedb73 89 if $running && !$restart;
e90ddc4c
FG
90
91 $targetsid = PVE::JSONSchema::map_id($self->{opts}->{storagemap}, $storage);
20ab40f3 92 }
6f42807e 93
0e0958a5
FG
94 if (!$remote) {
95 my $target_scfg = PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, $self->{node});
96
97 die "$volid: content type 'rootdir' is not available on storage '$targetsid'\n"
98 if !$target_scfg->{content}->{rootdir};
99 }
e90ddc4c 100
0e0958a5 101 $storages->{$targetsid} = 1;
6f42807e
DM
102 });
103
6f42807e
DM
104 # todo: test if VM uses local resources
105
0e0958a5
FG
106 if ($remote) {
107 # test & establish websocket connection
108 my $bridges = map_bridges($conf, $self->{opts}->{bridgemap}, 1);
109
110 my $remote = $self->{opts}->{remote};
111 my $conn = $remote->{conn};
112
113 my $log = sub {
114 my ($level, $msg) = @_;
115 $self->log($level, $msg);
116 };
117
118 my $websocket_url = "https://$conn->{host}:$conn->{port}/api2/json/nodes/$self->{node}/lxc/$remote->{vmid}/mtunnelwebsocket";
119 my $url = "/nodes/$self->{node}/lxc/$remote->{vmid}/mtunnel";
120
121 my $tunnel_params = {
122 url => $websocket_url,
123 };
124
125 my $storage_list = join(',', keys %$storages);
126 my $bridge_list = join(',', keys %$bridges);
127
128 my $req_params = {
129 storages => $storage_list,
130 bridges => $bridge_list,
131 };
132
133 my $tunnel = PVE::Tunnel::fork_websocket_tunnel($conn, $url, $req_params, $tunnel_params, $log);
134 my $min_version = $tunnel->{version} - $tunnel->{age};
135 $self->log('info', "local WS tunnel version: $WS_TUNNEL_VERSION");
136 $self->log('info', "remote WS tunnel version: $tunnel->{version}");
137 $self->log('info', "minimum required WS tunnel version: $min_version");
138 die "Remote tunnel endpoint not compatible, upgrade required\n"
139 if $WS_TUNNEL_VERSION < $min_version;
140 die "Remote tunnel endpoint too old, upgrade required\n"
141 if $WS_TUNNEL_VERSION > $tunnel->{version};
142
143 $self->log('info', "websocket tunnel started\n");
144 $self->{tunnel} = $tunnel;
145 } else {
146 # test ssh connection
147 my $cmd = [ @{$self->{rem_ssh}}, '/bin/true' ];
148 eval { $self->cmd_quiet($cmd); };
149 die "Can't connect to destination address using public key\n" if $@;
150 }
6f42807e 151
a7cedb73
DC
152 # in restart mode, we shutdown the container before migrating
153 if ($restart && $running) {
154 my $timeout = $self->{opts}->{timeout} // 180;
155
156 $self->log('info', "shutdown CT $vmid\n");
157
b1bad293 158 PVE::LXC::vm_stop($vmid, 0, $timeout);
a7cedb73
DC
159
160 $running = 0;
161 }
162
6f42807e
DM
163 return $running;
164}
165
166sub phase1 {
167 my ($self, $vmid) = @_;
168
0e0958a5
FG
169 my $remote = $self->{opts}->{remote};
170
6f42807e
DM
171 $self->log('info', "starting migration of CT $self->{vmid} to node '$self->{node}' ($self->{nodeip})");
172
173 my $conf = $self->{vmconf};
174 $conf->{lock} = 'migrate';
67afe46e 175 PVE::LXC::Config->write_config($vmid, $conf);
6f42807e
DM
176
177 if ($self->{running}) {
178 $self->log('info', "container is running - using online migration");
179 }
180
3c5dabe1 181 $self->{volumes} = []; # list of already migrated volumes
f503617b 182 my $volhash = {}; # 'config', 'snapshot' or 'storage' for local volumes
33deb76c
FG
183 my $volhash_errors = {};
184 my $abort = 0;
b3e73045 185 my $path_to_volid = {};
33deb76c
FG
186
187 my $log_error = sub {
188 my ($msg, $volid) = @_;
189
190 $volhash_errors->{$volid} = $msg if !defined($volhash_errors->{$volid});
191 $abort = 1;
192 };
6f42807e 193
3c5dabe1
FG
194 my $test_volid = sub {
195 my ($volid, $snapname) = @_;
6f42807e 196
3c5dabe1
FG
197 return if !$volid;
198
d87a7431
AL
199 # check if volume exists, might be a pending new one
200 if ($volid =~ $PVE::LXC::NEW_DISK_RE) {
201 $self->log('info', "volume '$volid' does not exist (pending change?)");
202 return;
203 }
204
3c5dabe1
FG
205 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
206
e90ddc4c 207 # check if storage is available on source node
a1c27f86 208 my $scfg = PVE::Storage::storage_check_enabled($self->{storecfg}, $sid);
e90ddc4c
FG
209
210 my $targetsid = $sid;
3c5dabe1 211
0e0958a5 212 if ($scfg->{shared} && !$remote) {
33deb76c
FG
213 $self->log('info', "volume '$volid' is on shared storage '$sid'")
214 if !$snapname;
215 return;
e90ddc4c
FG
216 } else {
217 $targetsid = PVE::JSONSchema::map_id($self->{opts}->{storagemap}, $sid);
33deb76c 218 }
3c5dabe1 219
0e0958a5
FG
220 PVE::Storage::storage_check_enabled($self->{storecfg}, $targetsid, $self->{node})
221 if !$remote;
e90ddc4c
FG
222
223 my $bwlimit = $self->get_bwlimit($sid, $targetsid);
224
0aa2d2a2 225 $volhash->{$volid}->{ref} = defined($snapname) ? 'snapshot' : 'config';
7121bd69 226 $volhash->{$volid}->{snapshots} = 1 if defined($snapname);
e90ddc4c
FG
227 $volhash->{$volid}->{targetsid} = $targetsid;
228 $volhash->{$volid}->{bwlimit} = $bwlimit;
f503617b 229
3c5dabe1
FG
230 my ($path, $owner) = PVE::Storage::path($self->{storecfg}, $volid);
231
33deb76c 232 die "owned by other guest (owner = $owner)\n"
3c5dabe1 233 if !$owner || ($owner != $self->{vmid});
9746c095 234
b3e73045
AL
235 $path_to_volid->{$path}->{$volid} = 1;
236
3c5dabe1
FG
237 if (defined($snapname)) {
238 # we cannot migrate shapshots on local storage
026ff6fa
WB
239 # exceptions: 'zfspool', 'btrfs'
240 if ($scfg->{type} eq 'zfspool' || $scfg->{type} eq 'btrfs') {
3c5dabe1
FG
241 return;
242 }
33deb76c 243 die "non-migratable snapshot exists\n";
3c5dabe1
FG
244 }
245 };
246
247 my $test_mp = sub {
248 my ($ms, $mountpoint, $snapname) = @_;
249
250 my $volid = $mountpoint->{volume};
9746c095
FG
251 # already checked in prepare
252 if ($mountpoint->{type} ne 'volume') {
552e168f 253 $self->log('info', "ignoring shared '$mountpoint->{type}' mount point '$ms' ('$volid')")
3c5dabe1 254 if !$snapname;
9746c095
FG
255 return;
256 }
257
33deb76c
FG
258 eval {
259 &$test_volid($volid, $snapname);
0e0958a5
FG
260
261 die "remote migration with snapshots not supported yet\n"
262 if $remote && $snapname;
33deb76c 263 };
6f42807e 264
33deb76c 265 &$log_error($@, $volid) if $@;
3c5dabe1
FG
266 };
267
d87a7431 268 # first all volumes referenced in snapshots
1c5f78ef
FG
269 foreach my $snapname (keys %{$conf->{snapshots}}) {
270 &$test_volid($conf->{snapshots}->{$snapname}->{'vmstate'}, 0, undef)
271 if defined($conf->{snapshots}->{$snapname}->{'vmstate'});
015740e6 272 PVE::LXC::Config->foreach_volume($conf->{snapshots}->{$snapname}, $test_mp, $snapname);
1c5f78ef
FG
273 }
274
d87a7431
AL
275 # then all pending volumes
276 if (defined($conf->{pending}) && $conf->{pending}->%*) {
277 PVE::LXC::Config->foreach_volume($conf->{pending}, $test_mp);
278 }
279
2334b15d
FE
280 # finally all current volumes
281 PVE::LXC::Config->foreach_volume_full($conf, { include_unused => 1 }, $test_mp);
1c5f78ef 282
b3e73045
AL
283 for my $path (keys %$path_to_volid) {
284 my @volids = keys $path_to_volid->{$path}->%*;
285 die "detected not supported aliased volumes: '" . join("', '", @volids) . "'"
286 if (scalar @volids > 1);
287 }
288
3c5dabe1
FG
289 # additional checks for local storage
290 foreach my $volid (keys %$volhash) {
33deb76c
FG
291 eval {
292 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
293 my $scfg = PVE::Storage::storage_config($self->{storecfg}, $sid);
3c5dabe1 294
0e0958a5
FG
295 # TODO move to storage plugin layer?
296 my $migratable_storages = [
297 'dir',
298 'zfspool',
299 'lvmthin',
300 'lvm',
301 'btrfs',
302 ];
303 if ($remote) {
304 push @$migratable_storages, 'cifs';
305 push @$migratable_storages, 'nfs';
306 }
3c5dabe1 307
33deb76c 308 die "storage type '$scfg->{type}' not supported\n"
0e0958a5 309 if !grep { $_ eq $scfg->{type} } @$migratable_storages;
3c5dabe1 310
33deb76c
FG
311 # image is a linked clone on local storage, se we can't migrate.
312 if (my $basename = (PVE::Storage::parse_volname($self->{storecfg}, $volid))[3]) {
313 die "clone of '$basename'";
314 }
315 };
316 &$log_error($@, $volid) if $@;
3c5dabe1
FG
317 }
318
f503617b 319 foreach my $volid (sort keys %$volhash) {
0aa2d2a2
WB
320 my $ref = $volhash->{$volid}->{ref};
321 if ($ref eq 'storage') {
f503617b 322 $self->log('info', "found local volume '$volid' (via storage)\n");
0aa2d2a2 323 } elsif ($ref eq 'config') {
f503617b 324 $self->log('info', "found local volume '$volid' (in current VM config)\n");
0aa2d2a2 325 } elsif ($ref eq 'snapshot') {
f503617b
FG
326 $self->log('info', "found local volume '$volid' (referenced by snapshot(s))\n");
327 } else {
328 $self->log('info', "found local volume '$volid'\n");
329 }
330 }
331
33deb76c
FG
332 foreach my $volid (sort keys %$volhash_errors) {
333 $self->log('warn', "can't migrate local volume '$volid': $volhash_errors->{$volid}");
334 }
335
336 if ($abort) {
337 die "can't migrate CT - check log\n";
338 }
339
3efa5e3d
DM
340 my $rep_volumes;
341
342 my $rep_cfg = PVE::ReplicationConfig->new();
343
0e0958a5
FG
344 if ($remote) {
345 die "cannot remote-migrate replicated VM\n"
346 if $rep_cfg->check_for_existing_jobs($vmid, 1);
347 } elsif (my $jobcfg = $rep_cfg->find_local_replication_job($vmid, $self->{node})) {
3efa5e3d
DM
348 die "can't live migrate VM with replicated volumes\n" if $self->{running};
349 my $start_time = time();
350 my $logfunc = sub { my ($msg) = @_; $self->log('info', $msg); };
351 $rep_volumes = PVE::Replication::run_replication(
352 'PVE::LXC::Config', $jobcfg, $start_time, $start_time, $logfunc);
353 }
354
1159329a 355 my $opts = $self->{opts};
3c5dabe1 356 foreach my $volid (keys %$volhash) {
3efa5e3d 357 next if $rep_volumes->{$volid};
3c5dabe1 358 push @{$self->{volumes}}, $volid;
e90ddc4c 359
1159329a 360 # JSONSchema and get_bandwidth_limit use kbps - storage_migrate bps
e90ddc4c 361 my $bwlimit = $volhash->{$volid}->{bwlimit};
1159329a
SI
362 $bwlimit = $bwlimit * 1024 if defined($bwlimit);
363
e90ddc4c
FG
364 my $targetsid = $volhash->{$volid}->{targetsid};
365
366 my $new_volid = eval {
0e0958a5
FG
367 if ($remote) {
368 my $log = sub {
369 my ($level, $msg) = @_;
370 $self->log($level, $msg);
371 };
372
373 return PVE::StorageTunnel::storage_migrate(
374 $self->{tunnel},
375 $self->{storecfg},
376 $volid,
377 $self->{vmid},
378 $remote->{vmid},
379 $volhash->{$volid},
380 $log,
381 );
382 } else {
383 my $storage_migrate_opts = {
384 'ratelimit_bps' => $bwlimit,
385 'insecure' => $opts->{migration_type} eq 'insecure',
386 'with_snapshots' => $volhash->{$volid}->{snapshots},
387 'allow_rename' => 1,
388 };
389
390 my $logfunc = sub { $self->log('info', $_[0]); };
391 return PVE::Storage::storage_migrate(
392 $self->{storecfg},
393 $volid,
394 $self->{ssh_info},
395 $targetsid,
396 $storage_migrate_opts,
397 $logfunc,
398 );
399 }
1d26bb86
FE
400 };
401
d79b051a 402 if (my $err = $@) {
e90ddc4c 403 die "storage migration for '$volid' to storage '$targetsid' failed - $err\n";
d79b051a 404 }
1e5f5da7 405
e90ddc4c
FG
406 $self->{volume_map}->{$volid} = $new_volid;
407 $self->log('info', "volume '$volid' is '$new_volid' on the target\n");
408
1e5f5da7
FE
409 eval { PVE::Storage::deactivate_volumes($self->{storecfg}, [$volid]); };
410 if (my $err = $@) {
411 $self->log('warn', $err);
412 }
3c5dabe1 413 }
6f42807e 414
6f42807e
DM
415 if ($self->{running}) {
416 die "implement me";
417 }
418
419 # make sure everything on (shared) storage is unmounted
420 # Note: we must be 100% sure, else we get data corruption because
421 # non-shared file system could be mounted twice (on shared storage)
422
423 PVE::LXC::umount_all($vmid, $self->{storecfg}, $conf);
424
c9bc5018 425 #to be sure there are no active volumes
d250604f 426 my $vollist = PVE::LXC::Config->get_vm_volumes($conf);
c9bc5018
WL
427 PVE::Storage::deactivate_volumes($self->{storecfg}, $vollist);
428
0e0958a5
FG
429 if ($remote) {
430 my $remote_conf = PVE::LXC::Config->load_config($vmid);
431 PVE::LXC::Config->update_volume_ids($remote_conf, $self->{volume_map});
432
433 my $bridges = map_bridges($remote_conf, $self->{opts}->{bridgemap});
434 for my $target (keys $bridges->%*) {
435 for my $nic (keys $bridges->{$target}->%*) {
436 $self->log('info', "mapped: $nic from $bridges->{$target}->{$nic} to $target");
437 }
438 }
439 my $conf_str = PVE::LXC::Config::write_pct_config("remote", $remote_conf);
440
441 # TODO expose in PVE::Firewall?
442 my $vm_fw_conf_path = "/etc/pve/firewall/$vmid.fw";
443 my $fw_conf_str;
444 $fw_conf_str = PVE::Tools::file_get_contents($vm_fw_conf_path)
445 if -e $vm_fw_conf_path;
446 my $params = {
447 conf => $conf_str,
448 'firewall-config' => $fw_conf_str,
449 };
450
451 PVE::Tunnel::write_tunnel($self->{tunnel}, 10, 'config', $params);
452 } else {
453 # transfer replication state before moving config
454 $self->transfer_replication_state() if $rep_volumes;
455 PVE::LXC::Config->update_volume_ids($conf, $self->{volume_map});
456 PVE::LXC::Config->write_config($vmid, $conf);
457 PVE::LXC::Config->move_config_to_node($vmid, $self->{node});
458 $self->switch_replication_job_target() if $rep_volumes;
459 }
1d1c1b4f 460 $self->{conf_migrated} = 1;
6f42807e
DM
461}
462
463sub phase1_cleanup {
464 my ($self, $vmid, $err) = @_;
465
466 $self->log('info', "aborting phase 1 - cleanup resources");
467
468 if ($self->{volumes}) {
469 foreach my $volid (@{$self->{volumes}}) {
800f454d
FG
470 if (my $mapped_volume = $self->{volume_map}->{$volid}) {
471 $volid = $mapped_volume;
472 }
6f42807e
DM
473 $self->log('err', "found stale volume copy '$volid' on node '$self->{node}'");
474 # fixme: try to remove ?
475 }
476 }
0e0958a5
FG
477
478 if ($self->{opts}->{remote}) {
479 # cleans up remote volumes
480 PVE::Tunnel::finish_tunnel($self->{tunnel}, 1);
481 delete $self->{tunnel};
482 }
6f42807e
DM
483}
484
485sub phase3 {
486 my ($self, $vmid) = @_;
487
488 my $volids = $self->{volumes};
489
0e0958a5
FG
490 # handled below in final_cleanup
491 return if $self->{opts}->{remote};
492
6f42807e
DM
493 # destroy local copies
494 foreach my $volid (@$volids) {
8fdd1fc8
DM
495 eval { PVE::Storage::vdisk_free($self->{storecfg}, $volid); };
496 if (my $err = $@) {
497 $self->log('err', "removing local copy of '$volid' failed - $err");
498 $self->{errors} = 1;
499 last if $err =~ /^interrupted by signal$/;
6f42807e
DM
500 }
501 }
502}
503
504sub final_cleanup {
505 my ($self, $vmid) = @_;
506
507 $self->log('info', "start final cleanup");
508
1d1c1b4f 509 if (!$self->{conf_migrated}) {
dd3bcfb2 510 eval { PVE::LXC::Config->remove_lock($vmid, 'migrate'); };
1d1c1b4f
WL
511 if (my $err = $@) {
512 $self->log('err', $err);
513 }
675b1f96 514 # in restart mode, we start the container on the source node on migration error
6725e93c
AA
515 if ($self->{opts}->{restart} && $self->{was_running}) {
516 $self->log('info', "start container on source node");
517 my $skiplock = 1;
518 PVE::LXC::vm_start($vmid, $self->{vmconf}, $skiplock);
519 }
0e0958a5
FG
520 } elsif ($self->{opts}->{remote}) {
521 eval { PVE::Tunnel::write_tunnel($self->{tunnel}, 10, 'unlock') };
522 $self->log('err', "Failed to clear migrate lock - $@\n") if $@;
523
524 if ($self->{opts}->{restart} && $self->{was_running}) {
525 $self->log('info', "start container on target node");
526 PVE::Tunnel::write_tunnel($self->{tunnel}, 60, 'start');
527 }
528 if ($self->{opts}->{delete}) {
529 PVE::LXC::destroy_lxc_container(
530 PVE::Storage::config(),
531 $vmid,
532 PVE::LXC::Config->load_config($vmid),
533 undef,
534 0,
535 );
536 }
537 PVE::Tunnel::finish_tunnel($self->{tunnel});
1d1c1b4f
WL
538 } else {
539 my $cmd = [ @{$self->{rem_ssh}}, 'pct', 'unlock', $vmid ];
ee8d9207
FG
540 $self->cmd_logerr($cmd, errmsg => "failed to clear migrate lock");
541
675b1f96 542 # in restart mode, we start the container on the target node after migration
ee8d9207
FG
543 if ($self->{opts}->{restart} && $self->{was_running}) {
544 $self->log('info', "start container on target node");
545 my $cmd = [ @{$self->{rem_ssh}}, 'pct', 'start', $vmid];
546 $self->cmd($cmd);
547 }
6f42807e 548 }
0e0958a5
FG
549}
550
551sub map_bridges {
552 my ($conf, $map, $scan_only) = @_;
553
554 my $bridges = {};
555
556 foreach my $opt (keys %$conf) {
557 next if $opt !~ m/^net\d+$/;
558
559 next if !$conf->{$opt};
560 my $d = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
561 next if !$d || !$d->{bridge};
562
563 my $target_bridge = PVE::JSONSchema::map_id($map, $d->{bridge});
564 $bridges->{$target_bridge}->{$opt} = $d->{bridge};
565
566 next if $scan_only;
567
568 $d->{bridge} = $target_bridge;
569 $conf->{$opt} = PVE::LXC::Config->print_lxc_network($d);
570 }
a7cedb73 571
0e0958a5 572 return $bridges;
6f42807e
DM
573}
574
5751;