1 use anyhow
::{bail, Error}
;
2 use serde_json
::{json, Value}
;
3 use ::serde
::{Deserialize, Serialize}
;
6 api
, Permission
, RpcEnvironment
, RpcEnvironmentType
,
13 parse_property_string
,
16 use proxmox
::api
::router
::Router
;
18 use crate::config
::acl
::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}
;
19 use crate::tools
::disks
::{
20 zpool_list
, zpool_status
, parse_zpool_status_config_tree
, vdev_list_to_tree
,
24 use crate::server
::WorkerTask
;
26 use crate::api2
::types
::*;
28 pub const DISK_ARRAY_SCHEMA
: Schema
= ArraySchema
::new(
29 "Disk name list.", &BLOCKDEVICE_NAME_SCHEMA
)
32 pub const DISK_LIST_SCHEMA
: Schema
= StringSchema
::new(
33 "A list of disk names, comma separated.")
34 .format(&ApiStringFormat
::PropertyString(&DISK_ARRAY_SCHEMA
))
37 pub const ZFS_ASHIFT_SCHEMA
: Schema
= IntegerSchema
::new(
38 "Pool sector size exponent.")
48 #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
49 #[serde(rename_all = "lowercase")]
50 /// The ZFS compression algorithm to use.
51 pub enum ZfsCompressionType
{
60 /// Enable compression using the default algorithm.
62 /// Disable compression.
67 #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
68 #[serde(rename_all = "lowercase")]
69 /// The ZFS RAID level to use.
70 pub enum ZfsRaidLevel
{
87 #[derive(Debug, Serialize, Deserialize)]
88 #[serde(rename_all="kebab-case")]
90 pub struct ZpoolListItem
{
101 /// ZFS fragnentation level
103 /// ZFS deduplication ratio
118 description
: "List of zpools.",
125 permission
: &Permission
::Privilege(&["system", "disks"], PRIV_SYS_AUDIT
, false),
129 pub fn list_zpools() -> Result
<Vec
<ZpoolListItem
>, Error
> {
131 let data
= zpool_list(None
, false)?
;
133 let mut list
= Vec
::new();
136 if let Some(usage
) = item
.usage
{
137 list
.push(ZpoolListItem
{
160 schema
: DATASTORE_SCHEMA
,
165 description
: "zpool vdev tree with status",
171 permission
: &Permission
::Privilege(&["system", "disks"], PRIV_SYS_AUDIT
, false),
174 /// Get zpool status details.
175 pub fn zpool_details(
177 ) -> Result
<Value
, Error
> {
179 let key_value_list
= zpool_status(&name
)?
;
181 let config
= match key_value_list
.iter().find(|(k
, _
)| k
== "config") {
183 None
=> bail
!("got zpool status without config key"),
186 let vdev_list
= parse_zpool_status_config_tree(config
)?
;
187 let mut tree
= vdev_list_to_tree(&vdev_list
)?
;
189 for (k
, v
) in key_value_list
{
195 tree
["name"] = tree
.as_object_mut().unwrap()
197 .unwrap_or(Value
::Null
);
211 schema
: DATASTORE_SCHEMA
,
214 schema
: DISK_LIST_SCHEMA
,
220 schema
: ZFS_ASHIFT_SCHEMA
,
224 type: ZfsCompressionType
,
228 description
: "Configure a datastore using the zpool.",
238 permission
: &Permission
::Privilege(&["system", "disks"], PRIV_SYS_MODIFY
, false),
241 /// Create a new ZFS pool.
245 raidlevel
: ZfsRaidLevel
,
246 compression
: Option
<String
>,
247 ashift
: Option
<usize>,
248 add_datastore
: Option
<bool
>,
249 rpcenv
: &mut dyn RpcEnvironment
,
250 ) -> Result
<String
, Error
> {
252 let to_stdout
= if rpcenv
.env_type() == RpcEnvironmentType
::CLI { true }
else { false }
;
254 let username
= rpcenv
.get_user().unwrap();
256 let add_datastore
= add_datastore
.unwrap_or(false);
258 let ashift
= ashift
.unwrap_or(12);
260 let devices_text
= devices
.clone();
261 let devices
= parse_property_string(&devices
, &DISK_ARRAY_SCHEMA
)?
;
262 let devices
: Vec
<String
> = devices
.as_array().unwrap().iter()
263 .map(|v
| v
.as_str().unwrap().to_string()).collect();
265 let disk_map
= crate::tools
::disks
::get_disks(None
, true)?
;
266 for disk
in devices
.iter() {
267 match disk_map
.get(disk
) {
269 if info
.used
!= DiskUsageType
::Unused
{
270 bail
!("disk '{}' is already in use.", disk
);
274 bail
!("no such disk '{}'", disk
);
279 let min_disks
= match raidlevel
{
280 ZfsRaidLevel
::Single
=> 1,
281 ZfsRaidLevel
::Mirror
=> 2,
282 ZfsRaidLevel
::Raid10
=> 4,
283 ZfsRaidLevel
::RaidZ
=> 3,
284 ZfsRaidLevel
::RaidZ2
=> 4,
285 ZfsRaidLevel
::RaidZ3
=> 5,
289 if raidlevel
== ZfsRaidLevel
::Raid10
&& devices
.len() % 2 != 0 {
290 bail
!("Raid10 needs an even number of disks.");
293 if raidlevel
== ZfsRaidLevel
::Single
&& devices
.len() > 1 {
294 bail
!("Please give only one disk for single disk mode.");
297 if devices
.len() < min_disks
{
298 bail
!("{:?} needs at least {} disks.", raidlevel
, min_disks
);
301 let upid_str
= WorkerTask
::new_thread(
302 "zfscreate", Some(name
.clone()), &username
.clone(), to_stdout
, move |worker
|
304 worker
.log(format
!("create {:?} zpool '{}' on devices '{}'", raidlevel
, name
, devices_text
));
307 let mut command
= std
::process
::Command
::new("zpool");
308 command
.args(&["create", "-o", &format
!("ashift={}", ashift
), &name
]);
311 ZfsRaidLevel
::Single
=> {
312 command
.arg(&devices
[0]);
314 ZfsRaidLevel
::Mirror
=> {
315 command
.arg("mirror");
316 command
.args(devices
);
318 ZfsRaidLevel
::Raid10
=> {
319 devices
.chunks(2).for_each(|pair
| {
320 command
.arg("mirror");
324 ZfsRaidLevel
::RaidZ
=> {
325 command
.arg("raidz");
326 command
.args(devices
);
328 ZfsRaidLevel
::RaidZ2
=> {
329 command
.arg("raidz2");
330 command
.args(devices
);
332 ZfsRaidLevel
::RaidZ3
=> {
333 command
.arg("raidz3");
334 command
.args(devices
);
338 worker
.log(format
!("# {:?}", command
));
340 let output
= crate::tools
::run_command(command
, None
)?
;
343 if let Some(compression
) = compression
{
344 let mut command
= std
::process
::Command
::new("zfs");
345 command
.args(&["set", &format
!("compression={}", compression
), &name
]);
346 worker
.log(format
!("# {:?}", command
));
347 let output
= crate::tools
::run_command(command
, None
)?
;
352 let mount_point
= format
!("/{}", name
);
353 crate::api2
::config
::datastore
::create_datastore(json
!({ "name": name, "path": mount_point }
))?
362 pub const POOL_ROUTER
: Router
= Router
::new()
363 .get(&API_METHOD_ZPOOL_DETAILS
);
365 pub const ROUTER
: Router
= Router
::new()
366 .get(&API_METHOD_LIST_ZPOOLS
)
367 .post(&API_METHOD_CREATE_ZPOOL
)
368 .match_all("name", &POOL_ROUTER
);