]>
git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Ceph.pm
1 package PVE
::API2
::CephOSD
;
9 use PVE
::Tools
qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach);
10 use PVE
::Exception
qw(raise raise_param_exc);
12 use PVE
::Cluster
qw(cfs_lock_file cfs_read_file cfs_write_file);
13 use PVE
::AccessControl
;
15 use PVE
::API2
::Storage
::Config
;
17 use PVE
::RPCEnvironment
;
18 use PVE
::JSONSchema
qw(get_standard_option);
23 use base
qw(PVE::RESTHandler);
25 use Data
::Dumper
; # fixme: remove
27 my $get_osd_status = sub {
28 my ($rados, $osdid) = @_;
30 my $stat = $rados->mon_command({ prefix
=> 'osd dump' });
32 my $osdlist = $stat->{osds
} || [];
34 my $flags = $stat->{flags
} || undef;
37 foreach my $d (@$osdlist) {
38 $osdstat->{$d->{osd
}} = $d if defined($d->{osd
});
40 if (defined($osdid)) {
41 die "no such OSD '$osdid'\n" if !$osdstat->{$osdid};
42 return $osdstat->{$osdid};
45 return wantarray?
($osdstat, $flags):$osdstat;
48 my $get_osd_usage = sub {
51 my $osdlist = $rados->mon_command({ prefix
=> 'pg dump',
52 dumpcontents
=> [ 'osds' ]}) || [];
55 foreach my $d (@$osdlist) {
56 $osdstat->{$d->{osd
}} = $d if defined($d->{osd
});
62 __PACKAGE__-
>register_method ({
66 description
=> "Get Ceph osd list/tree.",
70 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
73 additionalProperties
=> 0,
75 node
=> get_standard_option
('pve-node'),
78 # fixme: return a list instead of extjs tree format ?
85 PVE
::CephTools
::check_ceph_inited
();
87 my $rados = PVE
::RADOS-
>new();
88 my $res = $rados->mon_command({ prefix
=> 'osd tree' });
90 die "no tree nodes found\n" if !($res && $res->{nodes
});
92 my ($osdhash, $flags) = &$get_osd_status($rados);
94 my $usagehash = &$get_osd_usage($rados);
96 my $osdmetadata_tmp = $rados->mon_command({ prefix
=> 'osd metadata' });
99 foreach my $osd (@$osdmetadata_tmp) {
100 $osdmetadata->{$osd->{id
}} = $osd;
105 foreach my $e (@{$res->{nodes
}}) {
106 $nodes->{$e->{id
}} = $e;
114 foreach my $opt (qw(status crush_weight reweight device_class)) {
115 $new->{$opt} = $e->{$opt} if defined($e->{$opt});
118 if (my $stat = $osdhash->{$e->{id
}}) {
119 $new->{in} = $stat->{in} if defined($stat->{in});
122 if (my $stat = $usagehash->{$e->{id
}}) {
123 $new->{total_space
} = ($stat->{kb
} || 1) * 1024;
124 $new->{bytes_used
} = ($stat->{kb_used
} || 0) * 1024;
125 $new->{percent_used
} = ($new->{bytes_used
}*100)/$new->{total_space
};
126 if (my $d = $stat->{fs_perf_stat
}) {
127 $new->{commit_latency_ms
} = $d->{commit_latency_ms
};
128 $new->{apply_latency_ms
} = $d->{apply_latency_ms
};
132 my $osdmd = $osdmetadata->{$e->{id
}};
133 if ($e->{type
} eq 'osd' && $osdmd) {
134 if ($osdmd->{bluefs
}) {
135 $new->{osdtype
} = 'bluestore';
136 $new->{blfsdev
} = $osdmd->{bluestore_bdev_dev_node
};
137 $new->{dbdev
} = $osdmd->{bluefs_db_dev_node
};
138 $new->{waldev
} = $osdmd->{bluefs_wal_dev_node
};
140 $new->{osdtype
} = 'filestore';
144 $newnodes->{$e->{id
}} = $new;
147 foreach my $e (@{$res->{nodes
}}) {
148 my $new = $newnodes->{$e->{id
}};
149 if ($e->{children
} && scalar(@{$e->{children
}})) {
150 $new->{children
} = [];
152 foreach my $cid (@{$e->{children
}}) {
153 $nodes->{$cid}->{parent
} = $e->{id
};
154 if ($nodes->{$cid}->{type
} eq 'osd' &&
155 $e->{type
} eq 'host') {
156 $newnodes->{$cid}->{host
} = $e->{name
};
158 push @{$new->{children
}}, $newnodes->{$cid};
161 $new->{leaf
} = ($e->{id
} >= 0) ?
1 : 0;
166 foreach my $e (@{$res->{nodes
}}) {
167 if (!$nodes->{$e->{id
}}->{parent
}) {
168 push @$roots, $newnodes->{$e->{id
}};
172 die "no root node\n" if !@$roots;
174 my $data = { root
=> { leaf
=> 0, children
=> $roots } };
176 # we want this for the noout flag
177 $data->{flags
} = $flags if $flags;
182 __PACKAGE__-
>register_method ({
186 description
=> "Create OSD",
190 additionalProperties
=> 0,
192 node
=> get_standard_option
('pve-node'),
194 description
=> "Block device name.",
198 description
=> "Block device name for journal (filestore) or block.db (bluestore).",
203 description
=> "Block device name for block.wal (bluestore only).",
208 description
=> "File system type (filestore only).",
210 enum
=> ['xfs', 'ext4', 'btrfs'],
215 description
=> "Use bluestore instead of filestore.",
222 returns
=> { type
=> 'string' },
226 my $rpcenv = PVE
::RPCEnvironment
::get
();
228 my $authuser = $rpcenv->get_user();
230 raise_param_exc
({ 'bluestore' => "conflicts with parameter 'fstype'" })
231 if (defined($param->{fstype
}) && defined($param->{bluestore
}) && $param->{bluestore
});
233 PVE
::CephTools
::check_ceph_inited
();
235 PVE
::CephTools
::setup_pve_symlinks
();
237 PVE
::CephTools
::check_ceph_installed
('ceph_osd');
239 my $bluestore = $param->{bluestore
} // 0;
244 if ($param->{journal_dev
} && ($param->{journal_dev
} ne $param->{dev
})) {
245 $journal_dev = PVE
::Diskmanage
::verify_blockdev_path
($param->{journal_dev
});
246 # if only journal is given, also put the wal there
247 $wal_dev = $journal_dev;
250 if ($param->{wal_dev
} &&
251 ($param->{wal_dev
} ne $param->{dev
}) &&
252 (!$param->{journal_dev
} || $param->{wal_dev
} ne $param->{journal_dev
})) {
253 raise_param_exc
({ 'wal_dev' => "can only be set with paramater 'bluestore'"})
255 $wal_dev = PVE
::Diskmanage
::verify_blockdev_path
($param->{wal_dev
});
258 $param->{dev
} = PVE
::Diskmanage
::verify_blockdev_path
($param->{dev
});
260 my $devname = $param->{dev
};
261 $devname =~ s
|/dev/||;
263 my $disklist = PVE
::Diskmanage
::get_disks
($devname, 1);
265 my $diskinfo = $disklist->{$devname};
266 die "unable to get device info for '$devname'\n"
269 die "device '$param->{dev}' is in use\n"
270 if $diskinfo->{used
};
272 my $devpath = $diskinfo->{devpath
};
273 my $rados = PVE
::RADOS-
>new();
274 my $monstat = $rados->mon_command({ prefix
=> 'mon_status' });
275 die "unable to get fsid\n" if !$monstat->{monmap
} || !$monstat->{monmap
}->{fsid
};
277 my $fsid = $monstat->{monmap
}->{fsid
};
278 $fsid = $1 if $fsid =~ m/^([0-9a-f\-]+)$/;
280 my $ceph_bootstrap_osd_keyring = PVE
::CephTools
::get_config
('ceph_bootstrap_osd_keyring');
282 if (! -f
$ceph_bootstrap_osd_keyring) {
283 my $bindata = $rados->mon_command({ prefix
=> 'auth get', entity
=> 'client.bootstrap-osd', format
=> 'plain' });
284 PVE
::Tools
::file_set_contents
($ceph_bootstrap_osd_keyring, $bindata);
290 my $fstype = $param->{fstype
} || 'xfs';
293 my $ccname = PVE
::CephTools
::get_config
('ccname');
295 my $cmd = ['ceph-disk', 'prepare', '--zap-disk',
296 '--cluster', $ccname, '--cluster-uuid', $fsid ];
299 print "create OSD on $devpath (bluestore)\n";
300 push @$cmd, '--bluestore';
303 print "using device '$journal_dev' for block.db\n";
304 push @$cmd, '--block.db', $journal_dev;
308 print "using device '$wal_dev' for block.wal\n";
309 push @$cmd, '--block.wal', $wal_dev;
312 push @$cmd, $devpath;
314 print "create OSD on $devpath ($fstype)\n";
315 push @$cmd, '--filestore', '--fs-type', $fstype;
317 print "using device '$journal_dev' for journal\n";
318 push @$cmd, '--journal-dev', $devpath, $journal_dev;
320 push @$cmd, $devpath;
328 return $rpcenv->fork_worker('cephcreateosd', $devname, $authuser, $worker);
331 __PACKAGE__-
>register_method ({
332 name
=> 'destroyosd',
335 description
=> "Destroy OSD",
339 additionalProperties
=> 0,
341 node
=> get_standard_option
('pve-node'),
343 description
=> 'OSD ID',
347 description
=> "If set, we remove partition table entries.",
354 returns
=> { type
=> 'string' },
358 my $rpcenv = PVE
::RPCEnvironment
::get
();
360 my $authuser = $rpcenv->get_user();
362 PVE
::CephTools
::check_ceph_inited
();
364 my $osdid = $param->{osdid
};
366 my $rados = PVE
::RADOS-
>new();
367 my $osdstat = &$get_osd_status($rados, $osdid);
369 die "osd is in use (in == 1)\n" if $osdstat->{in};
370 #&$run_ceph_cmd(['osd', 'out', $osdid]);
372 die "osd is still runnung (up == 1)\n" if $osdstat->{up
};
374 my $osdsection = "osd.$osdid";
379 # reopen with longer timeout
380 $rados = PVE
::RADOS-
>new(timeout
=> PVE
::CephTools
::get_config
('long_rados_timeout'));
382 print "destroy OSD $osdsection\n";
384 eval { PVE
::CephTools
::ceph_service_cmd
('stop', $osdsection); };
387 print "Remove $osdsection from the CRUSH map\n";
388 $rados->mon_command({ prefix
=> "osd crush remove", name
=> $osdsection, format
=> 'plain' });
390 print "Remove the $osdsection authentication key.\n";
391 $rados->mon_command({ prefix
=> "auth del", entity
=> $osdsection, format
=> 'plain' });
393 print "Remove OSD $osdsection\n";
394 $rados->mon_command({ prefix
=> "osd rm", ids
=> [ $osdsection ], format
=> 'plain' });
396 # try to unmount from standard mount point
397 my $mountpoint = "/var/lib/ceph/osd/ceph-$osdid";
399 my $remove_partition = sub {
402 return if !$part || (! -b
$part );
403 my $partnum = PVE
::Diskmanage
::get_partnum
($part);
404 my $devpath = PVE
::Diskmanage
::get_blockdev
($part);
406 print "remove partition $part (disk '${devpath}', partnum $partnum)\n";
407 eval { run_command
(['/sbin/sgdisk', '-d', $partnum, "${devpath}"]); };
411 my $partitions_to_remove = [];
413 if ($param->{cleanup
}) {
414 if (my $fd = IO
::File-
>new("/proc/mounts", "r")) {
415 while (defined(my $line = <$fd>)) {
416 my ($dev, $path, $fstype) = split(/\s+/, $line);
417 next if !($dev && $path && $fstype);
418 next if $dev !~ m
|^/dev/|;
419 if ($path eq $mountpoint) {
420 my $data_part = abs_path
($dev);
421 push @$partitions_to_remove, $data_part;
428 foreach my $path (qw(journal block block.db block.wal)) {
429 my $part = abs_path
("$mountpoint/$path");
431 push @$partitions_to_remove, $part;
436 print "Unmount OSD $osdsection from $mountpoint\n";
437 eval { run_command
(['/bin/umount', $mountpoint]); };
440 } elsif ($param->{cleanup
}) {
441 #be aware of the ceph udev rules which can remount.
442 foreach my $part (@$partitions_to_remove) {
443 $remove_partition->($part);
448 return $rpcenv->fork_worker('cephdestroyosd', $osdsection, $authuser, $worker);
451 __PACKAGE__-
>register_method ({
453 path
=> '{osdid}/in',
455 description
=> "ceph osd in",
459 check
=> ['perm', '/', [ 'Sys.Modify' ]],
462 additionalProperties
=> 0,
464 node
=> get_standard_option
('pve-node'),
466 description
=> 'OSD ID',
471 returns
=> { type
=> "null" },
475 PVE
::CephTools
::check_ceph_inited
();
477 my $osdid = $param->{osdid
};
479 my $rados = PVE
::RADOS-
>new();
481 my $osdstat = &$get_osd_status($rados, $osdid); # osd exists?
483 my $osdsection = "osd.$osdid";
485 $rados->mon_command({ prefix
=> "osd in", ids
=> [ $osdsection ], format
=> 'plain' });
490 __PACKAGE__-
>register_method ({
492 path
=> '{osdid}/out',
494 description
=> "ceph osd out",
498 check
=> ['perm', '/', [ 'Sys.Modify' ]],
501 additionalProperties
=> 0,
503 node
=> get_standard_option
('pve-node'),
505 description
=> 'OSD ID',
510 returns
=> { type
=> "null" },
514 PVE
::CephTools
::check_ceph_inited
();
516 my $osdid = $param->{osdid
};
518 my $rados = PVE
::RADOS-
>new();
520 my $osdstat = &$get_osd_status($rados, $osdid); # osd exists?
522 my $osdsection = "osd.$osdid";
524 $rados->mon_command({ prefix
=> "osd out", ids
=> [ $osdsection ], format
=> 'plain' });
529 package PVE
::API2
::Ceph
;
535 use POSIX qw
(LONG_MAX
);
536 use Cwd
qw(abs_path);
542 use PVE
::Tools
qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach);
543 use PVE
::Exception
qw(raise raise_param_exc);
545 use PVE
::Cluster
qw(cfs_lock_file cfs_read_file cfs_write_file);
546 use PVE
::AccessControl
;
548 use PVE
::RESTHandler
;
549 use PVE
::RPCEnvironment
;
550 use PVE
::JSONSchema
qw(get_standard_option);
555 use base
qw(PVE::RESTHandler);
557 use Data
::Dumper
; # fixme: remove
559 my $pve_osd_default_journal_size = 1024*5;
561 __PACKAGE__-
>register_method ({
562 subclass
=> "PVE::API2::CephOSD",
566 __PACKAGE__-
>register_method ({
570 description
=> "Directory index.",
571 permissions
=> { user
=> 'all' },
573 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
576 additionalProperties
=> 0,
578 node
=> get_standard_option
('pve-node'),
587 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
599 { name
=> 'status' },
601 { name
=> 'config' },
611 __PACKAGE__-
>register_method ({
615 description
=> "List local disks.",
619 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
622 additionalProperties
=> 0,
624 node
=> get_standard_option
('pve-node'),
626 description
=> "Only list specific types of disks.",
628 enum
=> ['unused', 'journal_disks'],
638 dev
=> { type
=> 'string' },
639 used
=> { type
=> 'string', optional
=> 1 },
640 gpt
=> { type
=> 'boolean' },
641 size
=> { type
=> 'integer' },
642 osdid
=> { type
=> 'integer' },
643 vendor
=> { type
=> 'string', optional
=> 1 },
644 model
=> { type
=> 'string', optional
=> 1 },
645 serial
=> { type
=> 'string', optional
=> 1 },
648 # links => [ { rel => 'child', href => "{}" } ],
653 PVE
::CephTools
::check_ceph_inited
();
655 my $disks = PVE
::Diskmanage
::get_disks
(undef, 1);
658 foreach my $dev (keys %$disks) {
659 my $d = $disks->{$dev};
660 if ($param->{type
}) {
661 if ($param->{type
} eq 'journal_disks') {
662 next if $d->{osdid
} >= 0;
664 } elsif ($param->{type
} eq 'unused') {
667 die "internal error"; # should not happen
671 $d->{dev
} = "/dev/$dev";
678 __PACKAGE__-
>register_method ({
683 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
685 description
=> "Get Ceph configuration.",
687 additionalProperties
=> 0,
689 node
=> get_standard_option
('pve-node'),
692 returns
=> { type
=> 'string' },
696 PVE
::CephTools
::check_ceph_inited
();
698 my $path = PVE
::CephTools
::get_config
('pve_ceph_cfgpath');
699 return PVE
::Tools
::file_get_contents
($path);
703 my $add_storage = sub {
704 my ($pool, $storeid, $krbd) = @_;
706 my $storage_params = {
711 content
=> $krbd ?
'rootdir' : 'images',
714 PVE
::API2
::Storage
::Config-
>create($storage_params);
717 my $get_storages = sub {
720 my $cfg = PVE
::Storage
::config
();
722 my $storages = $cfg->{ids
};
724 foreach my $storeid (keys %$storages) {
725 my $curr = $storages->{$storeid};
726 $res->{$storeid} = $storages->{$storeid}
727 if $curr->{type
} eq 'rbd' && $pool eq $curr->{pool
};
733 __PACKAGE__-
>register_method ({
737 description
=> "Get Ceph monitor list.",
741 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
744 additionalProperties
=> 0,
746 node
=> get_standard_option
('pve-node'),
754 name
=> { type
=> 'string' },
755 addr
=> { type
=> 'string' },
758 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
763 PVE
::CephTools
::check_ceph_inited
();
767 my $cfg = PVE
::CephTools
::parse_ceph_config
();
770 foreach my $section (keys %$cfg) {
771 my $d = $cfg->{$section};
772 if ($section =~ m/^mon\.(\S+)$/) {
774 if ($d->{'mon addr'} && $d->{'host'}) {
775 $monhash->{$monid} = {
776 addr
=> $d->{'mon addr'},
777 host
=> $d->{'host'},
785 my $rados = PVE
::RADOS-
>new();
786 my $monstat = $rados->mon_command({ prefix
=> 'mon_status' });
787 my $mons = $monstat->{monmap
}->{mons
};
788 foreach my $d (@$mons) {
789 next if !defined($d->{name
});
790 $monhash->{$d->{name
}}->{rank
} = $d->{rank
};
791 $monhash->{$d->{name
}}->{addr
} = $d->{addr
};
792 if (grep { $_ eq $d->{rank
} } @{$monstat->{quorum
}}) {
793 $monhash->{$d->{name
}}->{quorum
} = 1;
799 return PVE
::RESTHandler
::hash_to_array
($monhash, 'name');
802 __PACKAGE__-
>register_method ({
806 description
=> "Create initial ceph default configuration and setup symlinks.",
810 check
=> ['perm', '/', [ 'Sys.Modify' ]],
813 additionalProperties
=> 0,
815 node
=> get_standard_option
('pve-node'),
817 description
=> "Use specific network for all ceph related traffic",
818 type
=> 'string', format
=> 'CIDR',
823 description
=> 'Targeted number of replicas per object',
831 description
=> 'Minimum number of available replicas per object to allow I/O',
839 description
=> "Placement group bits, used to specify the " .
840 "default number of placement groups.\n\nNOTE: 'osd pool " .
841 "default pg num' does not work for default pools.",
849 description
=> "Disable cephx authentification.\n\n" .
850 "WARNING: cephx is a security feature protecting against " .
851 "man-in-the-middle attacks. Only consider disabling cephx ".
852 "if your network is private!",
859 returns
=> { type
=> 'null' },
863 my $version = PVE
::CephTools
::get_local_version
(1);
865 if (!$version || $version < 12) {
866 die "Ceph Luminous required - please run 'pveceph install'\n";
868 PVE
::CephTools
::check_ceph_installed
('ceph_bin');
871 # simply load old config if it already exists
872 my $cfg = PVE
::CephTools
::parse_ceph_config
();
874 if (!$cfg->{global
}) {
879 UUID
::generate
($uuid);
880 UUID
::unparse
($uuid, $fsid);
882 my $auth = $param->{disable_cephx
} ?
'none' : 'cephx';
886 'auth cluster required' => $auth,
887 'auth service required' => $auth,
888 'auth client required' => $auth,
889 'osd journal size' => $pve_osd_default_journal_size,
890 'osd pool default size' => $param->{size
} // 3,
891 'osd pool default min size' => $param->{min_size
} // 2,
892 'mon allow pool delete' => 'true',
895 # this does not work for default pools
896 #'osd pool default pg num' => $pg_num,
897 #'osd pool default pgp num' => $pg_num,
900 $cfg->{global
}->{keyring
} = '/etc/pve/priv/$cluster.$name.keyring';
901 $cfg->{osd
}->{keyring
} = '/var/lib/ceph/osd/ceph-$id/keyring';
903 if ($param->{pg_bits
}) {
904 $cfg->{global
}->{'osd pg bits'} = $param->{pg_bits
};
905 $cfg->{global
}->{'osd pgp bits'} = $param->{pg_bits
};
908 if ($param->{network
}) {
909 $cfg->{global
}->{'public network'} = $param->{network
};
910 $cfg->{global
}->{'cluster network'} = $param->{network
};
913 PVE
::CephTools
::write_ceph_config
($cfg);
915 PVE
::CephTools
::setup_pve_symlinks
();
920 my $find_node_ip = sub {
923 my $net = Net
::IP-
>new($cidr) || die Net
::IP
::Error
() . "\n";
924 my $id = $net->version == 6 ?
'address6' : 'address';
926 my $config = PVE
::INotify
::read_file
('interfaces');
927 my $ifaces = $config->{ifaces
};
929 foreach my $iface (keys %$ifaces) {
930 my $d = $ifaces->{$iface};
932 my $a = Net
::IP-
>new($d->{$id});
934 return $d->{$id} if $net->overlaps($a);
937 die "unable to find local address within network '$cidr'\n";
940 my $create_mgr = sub {
941 my ($rados, $id) = @_;
943 my $clustername = PVE
::CephTools
::get_config
('ccname');
944 my $mgrdir = "/var/lib/ceph/mgr/$clustername-$id";
945 my $mgrkeyring = "$mgrdir/keyring";
946 my $mgrname = "mgr.$id";
948 die "ceph manager directory '$mgrdir' already exists\n"
951 print "creating manager directory '$mgrdir'\n";
953 print "creating keys for '$mgrname'\n";
954 my $output = $rados->mon_command({ prefix
=> 'auth get-or-create',
957 mon
=> 'allow profile mgr',
962 PVE
::Tools
::file_set_contents
($mgrkeyring, $output);
964 print "setting owner for directory\n";
965 run_command
(["chown", 'ceph:ceph', '-R', $mgrdir]);
967 print "enabling service 'ceph-mgr\@$id.service'\n";
968 PVE
::CephTools
::ceph_service_cmd
('enable', $mgrname);
969 print "starting service 'ceph-mgr\@$id.service'\n";
970 PVE
::CephTools
::ceph_service_cmd
('start', $mgrname);
973 my $destroy_mgr = sub {
976 my $clustername = PVE
::CephTools
::get_config
('ccname');
977 my $mgrname = "mgr.$mgrid";
978 my $mgrdir = "/var/lib/ceph/mgr/$clustername-$mgrid";
980 die "ceph manager directory '$mgrdir' not found\n"
983 print "disabling service 'ceph-mgr\@$mgrid.service'\n";
984 PVE
::CephTools
::ceph_service_cmd
('disable', $mgrname);
985 print "stopping service 'ceph-mgr\@$mgrid.service'\n";
986 PVE
::CephTools
::ceph_service_cmd
('stop', $mgrname);
988 print "removing manager directory '$mgrdir'\n";
989 File
::Path
::remove_tree
($mgrdir);
992 __PACKAGE__-
>register_method ({
996 description
=> "Create Ceph Monitor and Manager",
1000 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1003 additionalProperties
=> 0,
1005 node
=> get_standard_option
('pve-node'),
1009 pattern
=> '[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?',
1010 description
=> "The ID for the monitor, when omitted the same as the nodename",
1012 'exclude-manager' => {
1016 description
=> "When set, only a monitor will be created.",
1020 returns
=> { type
=> 'string' },
1024 PVE
::CephTools
::check_ceph_installed
('ceph_mon');
1026 PVE
::CephTools
::check_ceph_installed
('ceph_mgr')
1027 if (!$param->{'exclude-manager'});
1029 PVE
::CephTools
::check_ceph_inited
();
1031 PVE
::CephTools
::setup_pve_symlinks
();
1033 my $rpcenv = PVE
::RPCEnvironment
::get
();
1035 my $authuser = $rpcenv->get_user();
1037 my $cfg = PVE
::CephTools
::parse_ceph_config
();
1041 my $monaddrhash = {};
1043 my $systemd_managed = PVE
::CephTools
::systemd_managed
();
1045 foreach my $section (keys %$cfg) {
1046 next if $section eq 'global';
1047 my $d = $cfg->{$section};
1048 if ($section =~ m/^mon\./) {
1050 if ($d->{'mon addr'}) {
1051 $monaddrhash->{$d->{'mon addr'}} = $section;
1056 my $monid = $param->{id
} // $param->{node
};
1058 my $monsection = "mon.$monid";
1060 if (my $pubnet = $cfg->{global
}->{'public network'}) {
1061 $ip = &$find_node_ip($pubnet);
1063 $ip = PVE
::Cluster
::remote_node_ip
($param->{node
});
1066 my $monaddr = Net
::IP
::ip_is_ipv6
($ip) ?
"[$ip]:6789" : "$ip:6789";
1067 my $monname = $param->{node
};
1069 die "monitor '$monsection' already exists\n" if $cfg->{$monsection};
1070 die "monitor address '$monaddr' already in use by '$monaddrhash->{$monaddr}'\n"
1071 if $monaddrhash->{$monaddr};
1076 my $pve_ckeyring_path = PVE
::CephTools
::get_config
('pve_ckeyring_path');
1078 if (! -f
$pve_ckeyring_path) {
1079 run_command
("ceph-authtool $pve_ckeyring_path --create-keyring " .
1080 "--gen-key -n client.admin");
1083 my $pve_mon_key_path = PVE
::CephTools
::get_config
('pve_mon_key_path');
1084 if (! -f
$pve_mon_key_path) {
1085 run_command
("cp $pve_ckeyring_path $pve_mon_key_path.tmp");
1086 run_command
("ceph-authtool $pve_mon_key_path.tmp -n client.admin --set-uid=0 " .
1087 "--cap mds 'allow' " .
1088 "--cap osd 'allow *' " .
1089 "--cap mgr 'allow *' " .
1090 "--cap mon 'allow *'");
1091 run_command
("cp $pve_mon_key_path.tmp /etc/ceph/ceph.client.admin.keyring") if $systemd_managed;
1092 run_command
("chown ceph:ceph /etc/ceph/ceph.client.admin.keyring") if $systemd_managed;
1093 run_command
("ceph-authtool $pve_mon_key_path.tmp --gen-key -n mon. --cap mon 'allow *'");
1094 run_command
("mv $pve_mon_key_path.tmp $pve_mon_key_path");
1097 my $ccname = PVE
::CephTools
::get_config
('ccname');
1099 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
1100 -d
$mondir && die "monitor filesystem '$mondir' already exist\n";
1102 my $monmap = "/tmp/monmap";
1107 run_command
("chown ceph:ceph $mondir") if $systemd_managed;
1109 if ($moncount > 0) {
1110 my $rados = PVE
::RADOS-
>new(timeout
=> PVE
::CephTools
::get_config
('long_rados_timeout'));
1111 my $mapdata = $rados->mon_command({ prefix
=> 'mon getmap', format
=> 'plain' });
1112 PVE
::Tools
::file_set_contents
($monmap, $mapdata);
1114 run_command
("monmaptool --create --clobber --add $monid $monaddr --print $monmap");
1117 run_command
("ceph-mon --mkfs -i $monid --monmap $monmap --keyring $pve_mon_key_path");
1118 run_command
("chown ceph:ceph -R $mondir") if $systemd_managed;
1123 File
::Path
::remove_tree
($mondir);
1127 $cfg->{$monsection} = {
1129 'mon addr' => $monaddr,
1132 PVE
::CephTools
::write_ceph_config
($cfg);
1134 my $create_keys_pid = fork();
1135 if (!defined($create_keys_pid)) {
1136 die "Could not spawn ceph-create-keys to create bootstrap keys\n";
1137 } elsif ($create_keys_pid == 0) {
1138 exit PVE
::Tools
::run_command
(['ceph-create-keys', '-i', $monid]);
1140 PVE
::CephTools
::ceph_service_cmd
('start', $monsection);
1142 if ($systemd_managed) {
1143 #to ensure we have the correct startup order.
1144 eval { PVE
::Tools
::run_command
(['/bin/systemctl', 'enable', "ceph-mon\@${monid}.service"]); };
1145 warn "Enable ceph-mon\@${monid}.service manually"if $@;
1147 waitpid($create_keys_pid, 0);
1151 if (!$param->{'exclude-manager'}) {
1152 my $rados = PVE
::RADOS-
>new(timeout
=> PVE
::CephTools
::get_config
('long_rados_timeout'));
1153 $create_mgr->($rados, $monid);
1157 return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker);
1160 __PACKAGE__-
>register_method ({
1161 name
=> 'destroymon',
1162 path
=> 'mon/{monid}',
1164 description
=> "Destroy Ceph Monitor and Manager.",
1168 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1171 additionalProperties
=> 0,
1173 node
=> get_standard_option
('pve-node'),
1175 description
=> 'Monitor ID',
1177 pattern
=> '[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?',
1179 'exclude-manager' => {
1183 description
=> "When set, removes only the monitor, not the manager"
1187 returns
=> { type
=> 'string' },
1191 my $rpcenv = PVE
::RPCEnvironment
::get
();
1193 my $authuser = $rpcenv->get_user();
1195 PVE
::CephTools
::check_ceph_inited
();
1197 my $cfg = PVE
::CephTools
::parse_ceph_config
();
1199 my $monid = $param->{monid
};
1200 my $monsection = "mon.$monid";
1202 my $rados = PVE
::RADOS-
>new();
1203 my $monstat = $rados->mon_command({ prefix
=> 'mon_status' });
1204 my $monlist = $monstat->{monmap
}->{mons
};
1206 die "no such monitor id '$monid'\n"
1207 if !defined($cfg->{$monsection});
1209 my $ccname = PVE
::CephTools
::get_config
('ccname');
1211 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
1212 -d
$mondir || die "monitor filesystem '$mondir' does not exist on this node\n";
1214 die "can't remove last monitor\n" if scalar(@$monlist) <= 1;
1219 # reopen with longer timeout
1220 $rados = PVE
::RADOS-
>new(timeout
=> PVE
::CephTools
::get_config
('long_rados_timeout'));
1222 $rados->mon_command({ prefix
=> "mon remove", name
=> $monid, format
=> 'plain' });
1224 eval { PVE
::CephTools
::ceph_service_cmd
('stop', $monsection); };
1227 delete $cfg->{$monsection};
1228 PVE
::CephTools
::write_ceph_config
($cfg);
1229 File
::Path
::remove_tree
($mondir);
1232 if (!$param->{'exclude-manager'}) {
1233 eval { $destroy_mgr->($monid); };
1238 return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker);
1241 __PACKAGE__-
>register_method ({
1242 name
=> 'createmgr',
1245 description
=> "Create Ceph Manager",
1249 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1252 additionalProperties
=> 0,
1254 node
=> get_standard_option
('pve-node'),
1258 pattern
=> '[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?',
1259 description
=> "The ID for the manager, when omitted the same as the nodename",
1263 returns
=> { type
=> 'string' },
1267 PVE
::CephTools
::check_ceph_installed
('ceph_mgr');
1269 PVE
::CephTools
::check_ceph_inited
();
1271 my $rpcenv = PVE
::RPCEnvironment
::get
();
1273 my $authuser = $rpcenv->get_user();
1275 my $mgrid = $param->{id
} // $param->{node
};
1280 my $rados = PVE
::RADOS-
>new(timeout
=> PVE
::CephTools
::get_config
('long_rados_timeout'));
1282 $create_mgr->($rados, $mgrid);
1285 return $rpcenv->fork_worker('cephcreatemgr', "mgr.$mgrid", $authuser, $worker);
1288 __PACKAGE__-
>register_method ({
1289 name
=> 'destroymgr',
1292 description
=> "Destroy Ceph Manager.",
1296 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1299 additionalProperties
=> 0,
1301 node
=> get_standard_option
('pve-node'),
1303 description
=> 'The ID of the manager',
1305 pattern
=> '[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?',
1309 returns
=> { type
=> 'string' },
1313 my $rpcenv = PVE
::RPCEnvironment
::get
();
1315 my $authuser = $rpcenv->get_user();
1317 PVE
::CephTools
::check_ceph_inited
();
1319 my $mgrid = $param->{id
};
1324 $destroy_mgr->($mgrid);
1327 return $rpcenv->fork_worker('cephdestroymgr', "mgr.$mgrid", $authuser, $worker);
1330 __PACKAGE__-
>register_method ({
1334 description
=> "Stop ceph services.",
1338 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1341 additionalProperties
=> 0,
1343 node
=> get_standard_option
('pve-node'),
1345 description
=> 'Ceph service name.',
1348 pattern
=> '(mon|mds|osd|mgr)\.[A-Za-z0-9\-]{1,32}',
1352 returns
=> { type
=> 'string' },
1356 my $rpcenv = PVE
::RPCEnvironment
::get
();
1358 my $authuser = $rpcenv->get_user();
1360 PVE
::CephTools
::check_ceph_inited
();
1362 my $cfg = PVE
::CephTools
::parse_ceph_config
();
1363 scalar(keys %$cfg) || die "no configuration\n";
1369 if ($param->{service
}) {
1370 push @$cmd, $param->{service
};
1373 PVE
::CephTools
::ceph_service_cmd
(@$cmd);
1376 return $rpcenv->fork_worker('srvstop', $param->{service
} || 'ceph',
1377 $authuser, $worker);
1380 __PACKAGE__-
>register_method ({
1384 description
=> "Start ceph services.",
1388 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1391 additionalProperties
=> 0,
1393 node
=> get_standard_option
('pve-node'),
1395 description
=> 'Ceph service name.',
1398 pattern
=> '(mon|mds|osd|mgr)\.[A-Za-z0-9\-]{1,32}',
1402 returns
=> { type
=> 'string' },
1406 my $rpcenv = PVE
::RPCEnvironment
::get
();
1408 my $authuser = $rpcenv->get_user();
1410 PVE
::CephTools
::check_ceph_inited
();
1412 my $cfg = PVE
::CephTools
::parse_ceph_config
();
1413 scalar(keys %$cfg) || die "no configuration\n";
1418 my $cmd = ['start'];
1419 if ($param->{service
}) {
1420 push @$cmd, $param->{service
};
1423 PVE
::CephTools
::ceph_service_cmd
(@$cmd);
1426 return $rpcenv->fork_worker('srvstart', $param->{service
} || 'ceph',
1427 $authuser, $worker);
1430 __PACKAGE__-
>register_method ({
1434 description
=> "Get ceph status.",
1438 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
1441 additionalProperties
=> 0,
1443 node
=> get_standard_option
('pve-node'),
1446 returns
=> { type
=> 'object' },
1450 PVE
::CephTools
::check_ceph_enabled
();
1452 my $rados = PVE
::RADOS-
>new();
1453 my $status = $rados->mon_command({ prefix
=> 'status' });
1454 $status->{health
} = $rados->mon_command({ prefix
=> 'health', detail
=> 'detail' });
1458 __PACKAGE__-
>register_method ({
1462 description
=> "List all pools.",
1466 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
1469 additionalProperties
=> 0,
1471 node
=> get_standard_option
('pve-node'),
1479 pool
=> { type
=> 'integer' },
1480 pool_name
=> { type
=> 'string' },
1481 size
=> { type
=> 'integer' },
1484 links
=> [ { rel
=> 'child', href
=> "{pool_name}" } ],
1489 PVE
::CephTools
::check_ceph_inited
();
1491 my $rados = PVE
::RADOS-
>new();
1494 my $res = $rados->mon_command({ prefix
=> 'df' });
1495 my $total = $res->{stats
}->{total_avail_bytes
} || 0;
1497 foreach my $d (@{$res->{pools
}}) {
1498 next if !$d->{stats
};
1499 next if !defined($d->{id
});
1500 $stats->{$d->{id
}} = $d->{stats
};
1503 $res = $rados->mon_command({ prefix
=> 'osd dump' });
1504 my $rulestmp = $rados->mon_command({ prefix
=> 'osd crush rule dump'});
1507 for my $rule (@$rulestmp) {
1508 $rules->{$rule->{rule_id
}} = $rule->{rule_name
};
1512 foreach my $e (@{$res->{pools
}}) {
1514 foreach my $attr (qw(pool pool_name size min_size pg_num crush_rule)) {
1515 $d->{$attr} = $e->{$attr} if defined($e->{$attr});
1518 if (defined($d->{crush_rule
}) && defined($rules->{$d->{crush_rule
}})) {
1519 $d->{crush_rule_name
} = $rules->{$d->{crush_rule
}};
1522 if (my $s = $stats->{$d->{pool
}}) {
1523 $d->{bytes_used
} = $s->{bytes_used
};
1524 $d->{percent_used
} = ($s->{bytes_used
} / $total)*100
1525 if $s->{max_avail
} && $total;
1534 __PACKAGE__-
>register_method ({
1535 name
=> 'createpool',
1538 description
=> "Create POOL",
1542 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1545 additionalProperties
=> 0,
1547 node
=> get_standard_option
('pve-node'),
1549 description
=> "The name of the pool. It must be unique.",
1553 description
=> 'Number of replicas per object',
1561 description
=> 'Minimum number of replicas per object',
1569 description
=> "Number of placement groups.",
1577 description
=> "The rule to use for mapping object placement in the cluster.",
1582 description
=> "The application of the pool, 'rbd' by default.",
1584 enum
=> ['rbd', 'cephfs', 'rgw'],
1588 description
=> "Configure VM and CT storages using the new pool.",
1594 returns
=> { type
=> 'null' },
1598 PVE
::CephTools
::check_ceph_inited
();
1600 my $pve_ckeyring_path = PVE
::CephTools
::get_config
('pve_ckeyring_path');
1602 die "not fully configured - missing '$pve_ckeyring_path'\n"
1603 if ! -f
$pve_ckeyring_path;
1605 my $pool = $param->{name
};
1607 if ($param->{add_storages
}) {
1608 my $rpcenv = PVE
::RPCEnvironment
::get
();
1609 my $user = $rpcenv->get_user();
1610 $rpcenv->check($user, '/storage', ['Datastore.Allocate']);
1611 die "pool name contains characters which are illegal for storage naming\n"
1612 if !PVE
::JSONSchema
::parse_storage_id
($pool);
1615 my $pg_num = $param->{pg_num
} || 64;
1616 my $size = $param->{size
} || 3;
1617 my $min_size = $param->{min_size
} || 2;
1618 my $rados = PVE
::RADOS-
>new();
1619 my $application = $param->{application
} // 'rbd';
1621 $rados->mon_command({
1622 prefix
=> "osd pool create",
1624 pg_num
=> int($pg_num),
1628 $rados->mon_command({
1629 prefix
=> "osd pool set",
1636 $rados->mon_command({
1637 prefix
=> "osd pool set",
1644 if (defined($param->{crush_rule
})) {
1645 $rados->mon_command({
1646 prefix
=> "osd pool set",
1648 var
=> 'crush_rule',
1649 val
=> $param->{crush_rule
},
1654 $rados->mon_command({
1655 prefix
=> "osd pool application enable",
1657 app
=> $application,
1660 if ($param->{add_storages
}) {
1662 eval { $add_storage->($pool, "${pool}_vm", 0); };
1664 warn "failed to add VM storage: $@";
1667 eval { $add_storage->($pool, "${pool}_ct", 1); };
1669 warn "failed to add CT storage: $@";
1672 die "adding storages for pool '$pool' failed, check log and add manually!\n"
1679 __PACKAGE__-
>register_method ({
1680 name
=> 'get_flags',
1683 description
=> "get all set ceph flags",
1687 check
=> ['perm', '/', [ 'Sys.Audit' ]],
1690 additionalProperties
=> 0,
1692 node
=> get_standard_option
('pve-node'),
1695 returns
=> { type
=> 'string' },
1699 PVE
::CephTools
::check_ceph_inited
();
1701 my $pve_ckeyring_path = PVE
::CephTools
::get_config
('pve_ckeyring_path');
1703 die "not fully configured - missing '$pve_ckeyring_path'\n"
1704 if ! -f
$pve_ckeyring_path;
1706 my $rados = PVE
::RADOS-
>new();
1708 my $stat = $rados->mon_command({ prefix
=> 'osd dump' });
1710 return $stat->{flags
} // '';
1713 __PACKAGE__-
>register_method ({
1715 path
=> 'flags/{flag}',
1717 description
=> "Set a ceph flag",
1721 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1724 additionalProperties
=> 0,
1726 node
=> get_standard_option
('pve-node'),
1728 description
=> 'The ceph flag to set/unset',
1730 enum
=> [ 'full', 'pause', 'noup', 'nodown', 'noout', 'noin', 'nobackfill', 'norebalance', 'norecover', 'noscrub', 'nodeep-scrub', 'notieragent'],
1734 returns
=> { type
=> 'null' },
1738 PVE
::CephTools
::check_ceph_inited
();
1740 my $pve_ckeyring_path = PVE
::CephTools
::get_config
('pve_ckeyring_path');
1742 die "not fully configured - missing '$pve_ckeyring_path'\n"
1743 if ! -f
$pve_ckeyring_path;
1745 my $set = $param->{set
} // !$param->{unset
};
1746 my $rados = PVE
::RADOS-
>new();
1748 $rados->mon_command({
1749 prefix
=> "osd set",
1750 key
=> $param->{flag
},
1756 __PACKAGE__-
>register_method ({
1757 name
=> 'unset_flag',
1758 path
=> 'flags/{flag}',
1760 description
=> "Unset a ceph flag",
1764 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1767 additionalProperties
=> 0,
1769 node
=> get_standard_option
('pve-node'),
1771 description
=> 'The ceph flag to set/unset',
1773 enum
=> [ 'full', 'pause', 'noup', 'nodown', 'noout', 'noin', 'nobackfill', 'norebalance', 'norecover', 'noscrub', 'nodeep-scrub', 'notieragent'],
1777 returns
=> { type
=> 'null' },
1781 PVE
::CephTools
::check_ceph_inited
();
1783 my $pve_ckeyring_path = PVE
::CephTools
::get_config
('pve_ckeyring_path');
1785 die "not fully configured - missing '$pve_ckeyring_path'\n"
1786 if ! -f
$pve_ckeyring_path;
1788 my $set = $param->{set
} // !$param->{unset
};
1789 my $rados = PVE
::RADOS-
>new();
1791 $rados->mon_command({
1792 prefix
=> "osd unset",
1793 key
=> $param->{flag
},
1799 __PACKAGE__-
>register_method ({
1800 name
=> 'destroypool',
1801 path
=> 'pools/{name}',
1803 description
=> "Destroy pool",
1807 check
=> ['perm', '/', [ 'Sys.Modify' ]],
1810 additionalProperties
=> 0,
1812 node
=> get_standard_option
('pve-node'),
1814 description
=> "The name of the pool. It must be unique.",
1818 description
=> "If true, destroys pool even if in use",
1825 returns
=> { type
=> 'null' },
1829 PVE
::CephTools
::check_ceph_inited
();
1831 my $pool = $param->{name
};
1833 # if not forced, destroy ceph pool only when no
1834 # vm disks are on it anymore
1835 if (!$param->{force
}) {
1836 my $storagecfg = PVE
::Storage
::config
();
1837 foreach my $storageid (keys %{$storagecfg->{ids
}}) {
1838 my $storage = $storagecfg->{ids
}->{$storageid};
1839 next if $storage->{type
} ne 'rbd';
1840 next if $storage->{pool
} ne $pool;
1842 # check if any vm disks are on the pool
1843 my $res = PVE
::Storage
::vdisk_list
($storagecfg, $storageid);
1844 die "ceph pool '$pool' still in use by storage '$storageid'\n"
1845 if @{$res->{$storageid}} != 0;
1849 my $rados = PVE
::RADOS-
>new();
1850 # fixme: '--yes-i-really-really-mean-it'
1851 $rados->mon_command({
1852 prefix
=> "osd pool delete",
1855 sure
=> '--yes-i-really-really-mean-it',
1863 __PACKAGE__-
>register_method ({
1867 description
=> "Get OSD crush map",
1871 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
1874 additionalProperties
=> 0,
1876 node
=> get_standard_option
('pve-node'),
1879 returns
=> { type
=> 'string' },
1883 PVE
::CephTools
::check_ceph_inited
();
1885 # this produces JSON (difficult to read for the user)
1886 # my $txt = &$run_ceph_cmd_text(['osd', 'crush', 'dump'], quiet => 1);
1890 my $mapfile = "/var/tmp/ceph-crush.map.$$";
1891 my $mapdata = "/var/tmp/ceph-crush.txt.$$";
1893 my $rados = PVE
::RADOS-
>new();
1896 my $bindata = $rados->mon_command({ prefix
=> 'osd getcrushmap', format
=> 'plain' });
1897 PVE
::Tools
::file_set_contents
($mapfile, $bindata);
1898 run_command
(['crushtool', '-d', $mapfile, '-o', $mapdata]);
1899 $txt = PVE
::Tools
::file_get_contents
($mapdata);
1911 __PACKAGE__-
>register_method({
1915 description
=> "Read ceph log",
1918 check
=> ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
1922 additionalProperties
=> 0,
1924 node
=> get_standard_option
('pve-node'),
1943 description
=> "Line number",
1947 description
=> "Line text",
1956 my $rpcenv = PVE
::RPCEnvironment
::get
();
1957 my $user = $rpcenv->get_user();
1958 my $node = $param->{node
};
1960 my $logfile = "/var/log/ceph/ceph.log";
1961 my ($count, $lines) = PVE
::Tools
::dump_logfile
($logfile, $param->{start
}, $param->{limit
});
1963 $rpcenv->set_result_attrib('total', $count);
1968 __PACKAGE__-
>register_method ({
1972 description
=> "List ceph rules.",
1976 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
1979 additionalProperties
=> 0,
1981 node
=> get_standard_option
('pve-node'),
1990 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
1995 PVE
::CephTools
::check_ceph_inited
();
1997 my $rados = PVE
::RADOS-
>new();
1999 my $rules = $rados->mon_command({ prefix
=> 'osd crush rule ls' });
2003 foreach my $rule (@$rules) {
2004 push @$res, { name
=> $rule };