Skip to content

Commit 63d3d76

Browse files
feat(lvm/vg): create and destroy vgs
Rather than importing existing VGs we now create the PV's and the VG itself. On destruction, if the VG has no non-mayastor LVs then we destroy it, otherwise we leave it behind, untagged. Signed-off-by: Tiago Castro <tiagolobocastro@gmail.com>
1 parent e6bd5db commit 63d3d76

File tree

8 files changed

+222
-17
lines changed

8 files changed

+222
-17
lines changed

doc/lvm.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# LVM as an alternative backend to Mayastor (Experimental!)
2+
3+
Mayastor, a cloud-native declarative data plane written in Rust, aims to abstract storage resources
4+
and their differences through the data plane.
5+
6+
In this document, we’ll explore how to integrate Logical Volume Management (LVM) as an alternative
7+
backend for Mayastor pools.
8+
LVM, a mature and widely adopted storage management system in Linux environments, offers robustness
9+
and extensive features that can enhance Mayastor’s storage services.
10+
11+
## Motivation
12+
13+
LVM is a mature and widely adopted storage management system in Linux environments.
14+
While the SPDK Blobstore (LVS) has been a reliable option, integrating LVM as an alternative backend
15+
can captivate a broader audience due to its robustness and maturity, feature set,
16+
and community support make it an attractive choice for Mayastor users.
17+
By integrating LVM, we can also allow users to upgrade existing non-replicated LVM volumes
18+
(eg: lvm-localpv) seamlessly.
19+
20+
## Goals
21+
22+
Alternative Backend: Enable Mayastor to use LVM volume groups as an alternative backend for storage
23+
pools.
24+
Dynamic Volume Management: Leverage LVM’s volume management features (resizing, snapshots,
25+
thin provisioning) within Mayastor.
26+
Simplicity: Abstract LVM complexities from users while providing robust storage services.
27+
28+
### Supporting Changes
29+
1. Pools
30+
31+
Mayastor pools represent devices supplying persistent backing storage.
32+
Introduce a new pool type: LVM Pool.
33+
Users can create new Mayastor pools with the LVM backend type.
34+
35+
2. LVM Integration
36+
37+
Extend Mayastor to integrate with LVM.
38+
Implement LVM-specific logic for pool and replica creation, replica resizing, and snapshot management.
39+
Ensure seamless interaction between Mayastor and LVM.
40+
41+
3. Replication (HA)
42+
43+
Extend Mayastor’s replication mechanisms to work with LVM-backed logical units.
44+
In short, make LVM backed volumes highly available across nodes.
45+
46+
4. Volume Management
47+
48+
Mayastor will expose LVM volume management features through its pool API.
49+
Users can resize volumes online.
50+
Snapshots are managed transparently.
51+
52+
## Conclusion
53+
54+
By integrating LVM with Mayastor, you can leverage the benefits of both technologies. LVM provides dynamic volume management, while Mayastor abstracts storage complexities, allowing you to focus on your applications.
55+
Happy storage provisioning! 🚀
56+
57+
58+
```mermaid
59+
graph TD;
60+
subgraph Physical Volumes
61+
PV1["/dev/sda"] --> VG["Volume Group - VG"]
62+
PV2["/dev/sdb"] --> VG
63+
end
64+
65+
subgraph Volume Group - VG
66+
VG --> LV_1["replica for volume 1"]
67+
VG --> LV_2["replica for volume 2"]
68+
end
69+
70+
subgraph Mayastor Volume
71+
LV_1 --> VOL_1["1-replica volume 1"]
72+
end
73+
```

io-engine/src/core/env.rs

+7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use crate::{
4646
core::{
4747
nic,
4848
reactor::{Reactor, ReactorState, Reactors},
49+
runtime,
4950
Cores,
5051
MayastorFeatures,
5152
Mthread,
@@ -486,6 +487,12 @@ async fn do_shutdown(arg: *mut c_void) {
486487
}
487488
nexus::shutdown_nexuses().await;
488489
crate::lvs::Lvs::export_all().await;
490+
491+
runtime::spawn_await(async {
492+
crate::lvm::VolumeGroup::export_all().await;
493+
})
494+
.await;
495+
489496
unsafe {
490497
spdk_rpc_finish();
491498
spdk_subsystem_fini(Some(reactors_stop), arg);

io-engine/src/core/runtime.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
//! runtime to do whatever it needs to do. The tokio threads are
44
//! unaffinitized such that they do not run on any of our reactors.
55
6-
use futures::Future;
6+
use crate::core::Reactor;
7+
use futures::{channel::oneshot, Future};
78
use once_cell::sync::Lazy;
89
use tokio::task::JoinHandle;
910

@@ -14,6 +15,22 @@ pub fn spawn(f: impl Future<Output = ()> + Send + 'static) {
1415
RUNTIME.spawn(f);
1516
}
1617

18+
/// Spawn a future on the tokio runtime and await its completion.
19+
pub async fn spawn_await(f: impl Future<Output = ()> + Send + 'static) {
20+
let (s, r) = oneshot::channel();
21+
22+
RUNTIME.spawn(async move {
23+
f.await;
24+
25+
if let Ok(r) = Reactor::spawn_at_primary(async move {
26+
s.send(()).ok();
27+
}) {
28+
r.await.ok();
29+
}
30+
});
31+
r.await.ok();
32+
}
33+
1734
/// block on the given future until it completes
1835
pub fn block_on(f: impl Future<Output = ()> + Send + 'static) {
1936
RUNTIME.block_on(f);

io-engine/src/grpc/v1/lvm/pool.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl PoolRpc for PoolService {
5959
"an lvs pool already uses the disk",
6060
));
6161
};
62-
VolumeGroup::import_or_create(args)
62+
VolumeGroup::create(args)
6363
.await
6464
.map_err(Status::from)
6565
.map(Pool::from)

io-engine/src/grpc/v1/lvm/replica.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl ReplicaService {
114114
})?);
115115

116116
if let Err(error) = lvol.share_nvmf(Some(props)).await {
117-
error!("Failed to import lvol: {error}...");
117+
error!("Failed to share lvol: {error}...");
118118
if created {
119119
// if we have created it here, then let's undo it
120120
lvol.destroy().await.ok();

io-engine/src/lvm/cli.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,24 @@ impl CmnQueryArgs {
7878
/// The following commands implement the core LVM functionality.
7979
#[derive(AsRefStr, EnumString, Display)]
8080
enum LvmSubCmd {
81+
/// Initialize physical volume(s) for use by LVM.
82+
#[strum(serialize = "pvcreate")]
83+
PVCreate,
84+
/// Remove LVM label(s) from physical volume(s).
85+
#[strum(serialize = "pvremove")]
86+
PVRemove,
8187
/// Display information about volume groups.
8288
#[strum(serialize = "vgs")]
8389
VGList,
90+
/// Create a volume group.
91+
#[strum(serialize = "vgcreate")]
92+
VGCreate,
8493
/// Change volume group attributes.
8594
#[strum(serialize = "vgchange")]
8695
VGChange,
96+
/// Remove volume group(s).
97+
#[strum(serialize = "vgremove")]
98+
VGRemove,
8799
/// Create a logical volume.
88100
#[strum(serialize = "lvcreate")]
89101
LVCreate,
@@ -132,6 +144,18 @@ impl LvmCmd {
132144
cmder: Command::new(cmd),
133145
}
134146
}
147+
/// Prepare a `Command` for `LvmSubCmd::PVCreate`.
148+
pub(super) fn pv_create() -> Self {
149+
Self::new(LvmSubCmd::PVCreate.as_ref())
150+
}
151+
/// Prepare a `Command` for `LvmSubCmd::PVRemove`.
152+
pub(super) fn pv_remove() -> Self {
153+
Self::new(LvmSubCmd::PVRemove.as_ref())
154+
}
155+
/// Prepare a `Command` for `LvmSubCmd::VGCreate`.
156+
pub(super) fn vg_create() -> Self {
157+
Self::new(LvmSubCmd::VGCreate.as_ref())
158+
}
135159
/// Prepare a `Command` for `LvmSubCmd::VGList`.
136160
pub(super) fn vg_list() -> Self {
137161
Self::new(LvmSubCmd::VGList.as_ref())
@@ -140,6 +164,10 @@ impl LvmCmd {
140164
pub(super) fn vg_change(vg_name: &str) -> Self {
141165
Self::new(LvmSubCmd::VGChange.as_ref()).arg(vg_name)
142166
}
167+
/// Prepare a `Command` for `LvmSubCmd::VGRemove`.
168+
pub(super) fn vg_remove() -> Self {
169+
Self::new(LvmSubCmd::VGRemove.as_ref())
170+
}
143171
/// Prepare a `Command` for `LvmSubCmd::LVCreate`.
144172
pub(super) fn lv_create() -> Self {
145173
Self::new(LvmSubCmd::LVCreate.as_ref())
@@ -322,7 +350,10 @@ pub(super) mod de {
322350
where
323351
E: de::Error,
324352
{
325-
let iter = s.split(',').map(FromStr::from_str);
353+
let iter = s
354+
.split(',')
355+
.skip_while(|&x| x.is_empty())
356+
.map(FromStr::from_str);
326357
Result::from_iter(iter).map_err(de::Error::custom)
327358
}
328359
}

io-engine/src/lvm/lv_replica.rs

+15
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ pub struct LogicalVolume {
108108
#[serde(rename = "lv_tags")]
109109
#[serde(deserialize_with = "de::comma_separated")]
110110
tags: Vec<Property>,
111+
/// Tags of the parent VG converted to our properties.
112+
#[serde(rename = "vg_tags")]
113+
#[serde(deserialize_with = "de::comma_separated")]
114+
vg_tags: Vec<Property>,
111115

112116
#[serde(skip)]
113117
runtime: RunLogicalVolume,
@@ -267,6 +271,9 @@ impl LogicalVolume {
267271
/// The LV is then imported as an spdk BDEV, which allows it to be shared
268272
/// via nvmf or open locally (ex: by the nexus).
269273
pub(crate) async fn import(&mut self) -> Result<(), Error> {
274+
if !self.ours() || !self.vg_ours() {
275+
return Ok(());
276+
}
270277
self.import_bdev().await
271278
}
272279

@@ -555,6 +562,14 @@ impl LogicalVolume {
555562
pub(crate) fn uuid(&self) -> &str {
556563
&self.lv_name
557564
}
565+
/// Check if this LV is owned by us.
566+
pub(crate) fn ours(&self) -> bool {
567+
self.tags.contains(&Property::Lvm)
568+
}
569+
/// Check if the parent VG is owned by us.
570+
pub(crate) fn vg_ours(&self) -> bool {
571+
self.vg_tags.contains(&Property::Lvm)
572+
}
558573
/// Get the name of the logical volume.
559574
pub(crate) fn name(&self) -> &Option<String> {
560575
&self.name

io-engine/src/lvm/vg_pool.rs

+75-13
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,46 @@ impl VolumeGroup {
124124
.into_iter()
125125
// todo: not needed as we did the select?
126126
.filter(|vg| vg.matches(opts))
127-
.collect::<Vec<_>>();
127+
.fold(Vec::<VolumeGroup>::new(), |mut acc, vg| {
128+
match acc.iter_mut().find(|e_vg| e_vg.name == vg.name) {
129+
None => acc.push(vg),
130+
Some(e_vg) => {
131+
e_vg.disks.extend(vg.disks);
132+
}
133+
}
134+
acc
135+
});
128136

129137
Ok(vgs)
130138
}
131139

132140
/// Import a volume group with the name provided or create one with the name
133141
/// and disks provided currently only import is supported.
134-
pub(crate) async fn import_or_create(
135-
args: PoolArgs,
136-
) -> Result<VolumeGroup, Error> {
137-
let vg = Self::import_inner(args).await?;
142+
pub(crate) async fn create(args: PoolArgs) -> Result<VolumeGroup, Error> {
143+
let vg =
144+
match VolumeGroup::lookup(CmnQueryArgs::any().named(&args.name))
145+
.await
146+
{
147+
Ok(_) => Self::import_inner(args).await,
148+
Err(Error::NotFound {
149+
..
150+
}) => {
151+
LvmCmd::pv_create().args(&args.disks).run().await?;
152+
153+
LvmCmd::vg_create()
154+
.arg(&args.name)
155+
.tag(Property::Lvm)
156+
.args(args.disks)
157+
.run()
158+
.await?;
159+
let lookup = CmnQueryArgs::ours()
160+
.named(&args.name)
161+
.uuid_opt(&args.uuid);
162+
VolumeGroup::lookup(lookup).await
163+
}
164+
Err(error) => Err(error),
165+
}?;
166+
138167
info!("The lvm vg pool '{}' has been created", vg.name());
139168
Ok(vg)
140169
}
@@ -149,6 +178,14 @@ impl VolumeGroup {
149178
.with_vg(CmnQueryArgs::ours().uuid(self.uuid()).named(self.name()));
150179
LogicalVolume::list(&query).await
151180
}
181+
async fn list_foreign_lvs(&self) -> Result<Vec<LogicalVolume>, Error> {
182+
let query = super::QueryArgs::new()
183+
.with_lv(CmnQueryArgs::any())
184+
.with_vg(CmnQueryArgs::any().uuid(self.uuid()).named(self.name()));
185+
LogicalVolume::list(&query)
186+
.await
187+
.map(|lvs| lvs.into_iter().filter(|lv| !lv.ours()).collect())
188+
}
152189

153190
/// Import a volume group by its name, match the disks on the volume group
154191
/// and if true add our tag to the volume group to make it available
@@ -191,22 +228,33 @@ impl VolumeGroup {
191228
}
192229

193230
/// Delete the volume group.
194-
/// > Note: Currently the vg is only exported.
195-
/// todo: currently only exporting the VG.
231+
/// > Note: The Vg is first exported and then destroyed.
196232
pub(crate) async fn destroy(mut self) -> Result<(), Error> {
197-
// As currently only import of volume group is supported
198-
// exporting the volume group on destroy.
199-
let name = self.name().to_string();
200233
self.export().await?;
201-
info!("LVM pool '{name}' has been destroyed successfully");
234+
235+
let foreign_lvs = self.list_foreign_lvs().await?;
236+
let name = self.name().to_string();
237+
238+
if foreign_lvs.is_empty() {
239+
LvmCmd::vg_remove()
240+
.arg(format!("--select=vg_name={name}"))
241+
.arg("-y")
242+
.run()
243+
.await?;
244+
245+
LvmCmd::pv_remove().args(&self.disks).run().await?;
246+
247+
info!("LVM pool '{}' has been destroyed successfully", self.name());
248+
} else {
249+
warn!("LVM pool '{}' is not destroyed as it contains foreign lvs: {foreign_lvs:?}", self.name());
250+
}
202251
self.ptpl().destroy().ok();
203252
Ok(())
204253
}
205254

206255
/// Exports the volume group by unloading all logical volumes and finally
207256
/// removing our tag from it
208257
pub(crate) async fn export(&mut self) -> Result<(), Error> {
209-
// todo: unload all logical volumes from this VG
210258
let lvs = self.list_lvs().await?;
211259
for mut lv in lvs {
212260
lv.export_bdev().await?;
@@ -215,7 +263,21 @@ impl VolumeGroup {
215263
LvmCmd::vg_change(self.name())
216264
.untag(Property::Lvm)
217265
.run()
218-
.await
266+
.await?;
267+
268+
info!("LVM pool '{}' has been exported successfully", self.name);
269+
Ok(())
270+
}
271+
272+
/// Export all VG instances.
273+
pub(crate) async fn export_all() {
274+
let Ok(pools) = VolumeGroup::list(&CmnQueryArgs::ours()).await else {
275+
return;
276+
};
277+
278+
for mut pool in pools {
279+
pool.export().await.ok();
280+
}
219281
}
220282

221283
/// Create a logical volume in this volume group.

0 commit comments

Comments
 (0)