Merge pull request #496 from dave-tucker/program-from-pinned3

aya: Add from_pin for Programs
pull/519/head
Dave Tucker 2 years ago committed by GitHub
commit 811ab299de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,6 +2,7 @@
use std::{ use std::{
hash::Hash, hash::Hash,
os::unix::prelude::{AsRawFd, RawFd}, os::unix::prelude::{AsRawFd, RawFd},
path::Path,
}; };
use crate::{ use crate::{
@ -138,6 +139,23 @@ impl CgroupSkb {
pub fn detach(&mut self, link_id: CgroupSkbLinkId) -> Result<(), ProgramError> { pub fn detach(&mut self, link_id: CgroupSkbLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id) self.data.links.remove(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(
path: P,
expected_attach_type: CgroupSkbAttachType,
) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self {
data,
expected_attach_type: Some(expected_attach_type),
})
}
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]

@ -4,6 +4,7 @@ pub use aya_obj::programs::CgroupSockAttachType;
use std::{ use std::{
hash::Hash, hash::Hash,
os::unix::prelude::{AsRawFd, RawFd}, os::unix::prelude::{AsRawFd, RawFd},
path::Path,
}; };
use crate::{ use crate::{
@ -113,6 +114,20 @@ impl CgroupSock {
pub fn detach(&mut self, link_id: CgroupSockLinkId) -> Result<(), ProgramError> { pub fn detach(&mut self, link_id: CgroupSockLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id) self.data.links.remove(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(
path: P,
attach_type: CgroupSockAttachType,
) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, attach_type })
}
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]

@ -4,6 +4,7 @@ pub use aya_obj::programs::CgroupSockAddrAttachType;
use std::{ use std::{
hash::Hash, hash::Hash,
os::unix::prelude::{AsRawFd, RawFd}, os::unix::prelude::{AsRawFd, RawFd},
path::Path,
}; };
use crate::{ use crate::{
@ -119,6 +120,20 @@ impl CgroupSockAddr {
pub fn detach(&mut self, link_id: CgroupSockAddrLinkId) -> Result<(), ProgramError> { pub fn detach(&mut self, link_id: CgroupSockAddrLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id) self.data.links.remove(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(
path: P,
attach_type: CgroupSockAddrAttachType,
) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, attach_type })
}
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]

@ -4,6 +4,7 @@ pub use aya_obj::programs::CgroupSockoptAttachType;
use std::{ use std::{
hash::Hash, hash::Hash,
os::unix::prelude::{AsRawFd, RawFd}, os::unix::prelude::{AsRawFd, RawFd},
path::Path,
}; };
use crate::{ use crate::{
@ -114,6 +115,20 @@ impl CgroupSockopt {
pub fn detach(&mut self, link_id: CgroupSockoptLinkId) -> Result<(), ProgramError> { pub fn detach(&mut self, link_id: CgroupSockoptLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id) self.data.links.remove(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(
path: P,
attach_type: CgroupSockoptAttachType,
) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, attach_type })
}
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]

@ -1,5 +1,5 @@
//! Kernel space probes. //! Kernel space probes.
use std::io; use std::{io, path::Path};
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
@ -83,6 +83,17 @@ impl KProbe {
pub fn take_link(&mut self, link_id: KProbeLinkId) -> Result<KProbeLink, ProgramError> { pub fn take_link(&mut self, link_id: KProbeLinkId) -> Result<KProbeLink, ProgramError> {
self.data.take_link(link_id) self.data.take_link(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, kind })
}
} }
define_link_wrapper!( define_link_wrapper!(

@ -181,7 +181,7 @@ impl PinnedLink {
PinnedLink { inner: link, path } PinnedLink { inner: link, path }
} }
/// Creates a [`PinnedLink`] from a valid path on bpffs. /// Creates a [`crate::programs::links::PinnedLink`] from a valid path on bpffs.
pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, LinkError> { pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, LinkError> {
let path_string = CString::new(path.as_ref().to_string_lossy().to_string()).unwrap(); let path_string = CString::new(path.as_ref().to_string_lossy().to_string()).unwrap();
let fd = let fd =

@ -1,4 +1,5 @@
//! LSM probes. //! LSM probes.
use crate::{ use crate::{
generated::{bpf_attach_type::BPF_LSM_MAC, bpf_prog_type::BPF_PROG_TYPE_LSM}, generated::{bpf_attach_type::BPF_LSM_MAC, bpf_prog_type::BPF_PROG_TYPE_LSM},
obj::btf::{Btf, BtfKind}, obj::btf::{Btf, BtfKind},

@ -69,7 +69,7 @@ use std::{
ffi::CString, ffi::CString,
io, io,
os::unix::io::{AsRawFd, RawFd}, os::unix::io::{AsRawFd, RawFd},
path::Path, path::{Path, PathBuf},
}; };
use thiserror::Error; use thiserror::Error;
@ -108,8 +108,9 @@ use crate::{
obj::{self, btf::BtfError, Function, KernelVersion}, obj::{self, btf::BtfError, Function, KernelVersion},
pin::PinError, pin::PinError,
sys::{ sys::{
bpf_get_object, bpf_load_program, bpf_pin_object, bpf_prog_get_fd_by_id, bpf_btf_get_fd_by_id, bpf_get_object, bpf_load_program, bpf_pin_object,
bpf_prog_get_info_by_fd, bpf_prog_query, retry_with_verifier_logs, BpfLoadProgramAttrs, bpf_prog_get_fd_by_id, bpf_prog_get_info_by_fd, bpf_prog_query, retry_with_verifier_logs,
BpfLoadProgramAttrs,
}, },
util::VerifierLog, util::VerifierLog,
}; };
@ -337,33 +338,33 @@ impl Program {
} }
} }
/// Unload the program /// Unloads the program from the kernel.
fn unload(&mut self) -> Result<(), ProgramError> { pub fn unload(self) -> Result<(), ProgramError> {
match self { match self {
Program::KProbe(p) => p.unload(), Program::KProbe(mut p) => p.unload(),
Program::UProbe(p) => p.unload(), Program::UProbe(mut p) => p.unload(),
Program::TracePoint(p) => p.unload(), Program::TracePoint(mut p) => p.unload(),
Program::SocketFilter(p) => p.unload(), Program::SocketFilter(mut p) => p.unload(),
Program::Xdp(p) => p.unload(), Program::Xdp(mut p) => p.unload(),
Program::SkMsg(p) => p.unload(), Program::SkMsg(mut p) => p.unload(),
Program::SkSkb(p) => p.unload(), Program::SkSkb(mut p) => p.unload(),
Program::SockOps(p) => p.unload(), Program::SockOps(mut p) => p.unload(),
Program::SchedClassifier(p) => p.unload(), Program::SchedClassifier(mut p) => p.unload(),
Program::CgroupSkb(p) => p.unload(), Program::CgroupSkb(mut p) => p.unload(),
Program::CgroupSysctl(p) => p.unload(), Program::CgroupSysctl(mut p) => p.unload(),
Program::CgroupSockopt(p) => p.unload(), Program::CgroupSockopt(mut p) => p.unload(),
Program::LircMode2(p) => p.unload(), Program::LircMode2(mut p) => p.unload(),
Program::PerfEvent(p) => p.unload(), Program::PerfEvent(mut p) => p.unload(),
Program::RawTracePoint(p) => p.unload(), Program::RawTracePoint(mut p) => p.unload(),
Program::Lsm(p) => p.unload(), Program::Lsm(mut p) => p.unload(),
Program::BtfTracePoint(p) => p.unload(), Program::BtfTracePoint(mut p) => p.unload(),
Program::FEntry(p) => p.unload(), Program::FEntry(mut p) => p.unload(),
Program::FExit(p) => p.unload(), Program::FExit(mut p) => p.unload(),
Program::Extension(p) => p.unload(), Program::Extension(mut p) => p.unload(),
Program::CgroupSockAddr(p) => p.unload(), Program::CgroupSockAddr(mut p) => p.unload(),
Program::SkLookup(p) => p.unload(), Program::SkLookup(mut p) => p.unload(),
Program::CgroupSock(p) => p.unload(), Program::CgroupSock(mut p) => p.unload(),
Program::CgroupDevice(p) => p.unload(), Program::CgroupDevice(mut p) => p.unload(),
} }
} }
@ -401,16 +402,10 @@ impl Program {
} }
} }
impl Drop for Program {
fn drop(&mut self) {
let _ = self.unload();
}
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ProgramData<T: Link> { pub(crate) struct ProgramData<T: Link> {
pub(crate) name: Option<String>, pub(crate) name: Option<String>,
pub(crate) obj: obj::Program, pub(crate) obj: Option<obj::Program>,
pub(crate) fd: Option<RawFd>, pub(crate) fd: Option<RawFd>,
pub(crate) links: LinkMap<T>, pub(crate) links: LinkMap<T>,
pub(crate) expected_attach_type: Option<bpf_attach_type>, pub(crate) expected_attach_type: Option<bpf_attach_type>,
@ -419,6 +414,7 @@ pub(crate) struct ProgramData<T: Link> {
pub(crate) attach_prog_fd: Option<RawFd>, pub(crate) attach_prog_fd: Option<RawFd>,
pub(crate) btf_fd: Option<RawFd>, pub(crate) btf_fd: Option<RawFd>,
pub(crate) verifier_log_level: u32, pub(crate) verifier_log_level: u32,
pub(crate) path: Option<PathBuf>,
} }
impl<T: Link> ProgramData<T> { impl<T: Link> ProgramData<T> {
@ -430,7 +426,7 @@ impl<T: Link> ProgramData<T> {
) -> ProgramData<T> { ) -> ProgramData<T> {
ProgramData { ProgramData {
name, name,
obj, obj: Some(obj),
fd: None, fd: None,
links: LinkMap::new(), links: LinkMap::new(),
expected_attach_type: None, expected_attach_type: None,
@ -439,8 +435,68 @@ impl<T: Link> ProgramData<T> {
attach_prog_fd: None, attach_prog_fd: None,
btf_fd, btf_fd,
verifier_log_level, verifier_log_level,
path: None,
} }
} }
pub(crate) fn from_bpf_prog_info(
name: Option<String>,
fd: RawFd,
path: &Path,
info: bpf_prog_info,
) -> Result<ProgramData<T>, ProgramError> {
let attach_btf_id = if info.attach_btf_id > 0 {
Some(info.attach_btf_id)
} else {
None
};
let attach_btf_obj_fd = if info.attach_btf_obj_id > 0 {
let fd = bpf_btf_get_fd_by_id(info.attach_btf_obj_id).map_err(|io_error| {
ProgramError::SyscallError {
call: "bpf_btf_get_fd_by_id".to_string(),
io_error,
}
})?;
Some(fd as u32)
} else {
None
};
Ok(ProgramData {
name,
obj: None,
fd: Some(fd),
links: LinkMap::new(),
expected_attach_type: None,
attach_btf_obj_fd,
attach_btf_id,
attach_prog_fd: None,
btf_fd: None,
verifier_log_level: 0,
path: Some(path.to_path_buf()),
})
}
pub(crate) fn from_pinned_path<P: AsRef<Path>>(
path: P,
) -> Result<ProgramData<T>, ProgramError> {
let path_string =
CString::new(path.as_ref().as_os_str().to_string_lossy().as_bytes()).unwrap();
let fd =
bpf_get_object(&path_string).map_err(|(_, io_error)| ProgramError::SyscallError {
call: "bpf_obj_get".to_owned(),
io_error,
})? as RawFd;
let info = bpf_prog_get_info_by_fd(fd).map_err(|io_error| ProgramError::SyscallError {
call: "bpf_prog_get_info_by_fd".to_owned(),
io_error,
})?;
let info = ProgramInfo(info);
let name = info.name_as_str().map(|s| s.to_string());
ProgramData::from_bpf_prog_info(name, fd, path.as_ref(), info.0)
}
} }
impl<T: Link> ProgramData<T> { impl<T: Link> ProgramData<T> {
@ -493,6 +549,11 @@ fn load_program<T: Link>(
if fd.is_some() { if fd.is_some() {
return Err(ProgramError::AlreadyLoaded); return Err(ProgramError::AlreadyLoaded);
} }
if obj.is_none() {
// This program was loaded from a pin in bpffs
return Err(ProgramError::AlreadyLoaded);
}
let obj = obj.as_ref().unwrap();
let crate::obj::Program { let crate::obj::Program {
function: function:
Function { Function {
@ -621,6 +682,12 @@ macro_rules! impl_program_unload {
unload_program(&mut self.data) unload_program(&mut self.data)
} }
} }
impl Drop for $struct_name {
fn drop(&mut self) {
let _ = self.unload();
}
}
)+ )+
} }
} }
@ -703,8 +770,17 @@ macro_rules! impl_program_pin{
/// To remove the program, the file on the BPF filesystem must be removed. /// To remove the program, the file on the BPF filesystem must be removed.
/// Any directories in the the path provided should have been created by the caller. /// Any directories in the the path provided should have been created by the caller.
pub fn pin<P: AsRef<Path>>(&mut self, path: P) -> Result<(), PinError> { pub fn pin<P: AsRef<Path>>(&mut self, path: P) -> Result<(), PinError> {
self.data.path = Some(path.as_ref().to_path_buf());
pin_program(&mut self.data, path) pin_program(&mut self.data, path)
} }
/// Removes the pinned link from the filesystem.
pub fn unpin(mut self) -> Result<(), io::Error> {
if let Some(path) = self.data.path.take() {
std::fs::remove_file(path)?;
}
Ok(())
}
} }
)+ )+
} }
@ -737,6 +813,45 @@ impl_program_pin!(
CgroupDevice, CgroupDevice,
); );
macro_rules! impl_from_pin {
($($struct_name:ident),+ $(,)?) => {
$(
impl $struct_name {
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data })
}
}
)+
}
}
// Use impl_from_pin if the program doesn't require additional data
impl_from_pin!(
TracePoint,
SocketFilter,
Xdp,
SkMsg,
CgroupSysctl,
LircMode2,
PerfEvent,
Lsm,
RawTracePoint,
BtfTracePoint,
FEntry,
FExit,
Extension,
SkLookup,
SockOps,
CgroupDevice,
);
macro_rules! impl_try_from_program { macro_rules! impl_try_from_program {
($($ty:ident),+ $(,)?) => { ($($ty:ident),+ $(,)?) => {
$( $(
@ -849,7 +964,6 @@ impl ProgramInfo {
unsafe { unsafe {
libc::close(fd); libc::close(fd);
} }
Ok(ProgramInfo(info)) Ok(ProgramInfo(info))
} }
} }

@ -1,4 +1,5 @@
//! Perf event programs. //! Perf event programs.
pub use crate::generated::{ pub use crate::generated::{
perf_hw_cache_id, perf_hw_cache_op_id, perf_hw_cache_op_result_id, perf_hw_id, perf_sw_ids, perf_hw_cache_id, perf_hw_cache_op_id, perf_hw_cache_op_result_id, perf_hw_id, perf_sw_ids,
}; };

@ -1,6 +1,6 @@
//! Skskb programs. //! Skskb programs.
use std::os::unix::io::AsRawFd; use std::{os::unix::io::AsRawFd, path::Path};
use crate::{ use crate::{
generated::{ generated::{
@ -105,6 +105,17 @@ impl SkSkb {
pub fn take_link(&mut self, link_id: SkSkbLinkId) -> Result<SkSkbLink, ProgramError> { pub fn take_link(&mut self, link_id: SkSkbLinkId) -> Result<SkSkbLink, ProgramError> {
self.data.take_link(link_id) self.data.take_link(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P, kind: SkSkbKind) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, kind })
}
} }
define_link_wrapper!( define_link_wrapper!(

@ -4,6 +4,7 @@ use thiserror::Error;
use std::{ use std::{
ffi::{CStr, CString}, ffi::{CStr, CString},
io, io,
path::Path,
}; };
use crate::{ use crate::{
@ -190,6 +191,20 @@ impl SchedClassifier {
) -> Result<SchedClassifierLink, ProgramError> { ) -> Result<SchedClassifierLink, ProgramError> {
self.data.take_link(link_id) self.data.take_link(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
let cname = CString::new(data.name.clone().unwrap_or_default())
.unwrap()
.into_boxed_c_str();
Ok(Self { data, name: cname })
}
} }
#[derive(Debug, Hash, Eq, PartialEq)] #[derive(Debug, Hash, Eq, PartialEq)]

@ -1,4 +1,5 @@
//! BTF-enabled raw tracepoints. //! BTF-enabled raw tracepoints.
use crate::{ use crate::{
generated::{bpf_attach_type::BPF_TRACE_RAW_TP, bpf_prog_type::BPF_PROG_TYPE_TRACING}, generated::{bpf_attach_type::BPF_TRACE_RAW_TP, bpf_prog_type::BPF_PROG_TYPE_TRACING},
obj::btf::{Btf, BtfKind}, obj::btf::{Btf, BtfKind},

@ -136,6 +136,17 @@ impl UProbe {
pub fn take_link(&mut self, link_id: UProbeLinkId) -> Result<UProbeLink, ProgramError> { pub fn take_link(&mut self, link_id: UProbeLinkId) -> Result<UProbeLink, ProgramError> {
self.data.take_link(link_id) self.data.take_link(link_id)
} }
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path)?;
Ok(Self { data, kind })
}
} }
define_link_wrapper!( define_link_wrapper!(

@ -1,4 +1,4 @@
use std::{process::Command, thread, time}; use std::{convert::TryInto, process::Command, thread, time};
use aya::{ use aya::{
include_bytes_aligned, include_bytes_aligned,
@ -192,16 +192,36 @@ fn pin_lifecycle() {
let mut bpf = Bpf::load(bytes).unwrap(); let mut bpf = Bpf::load(bytes).unwrap();
let prog: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); let prog: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap();
prog.load().unwrap(); prog.load().unwrap();
prog.pin("/sys/fs/bpf/aya-xdp-test-prog").unwrap();
}
// should still be loaded since prog was pinned
assert_loaded!("pass", true);
// 2. Load program from bpffs but don't attach it
{
let _ = Xdp::from_pin("/sys/fs/bpf/aya-xdp-test-prog").unwrap();
}
// should still be loaded since prog was pinned
assert_loaded!("pass", true);
// 3. Load program from bpffs and attach
{
let mut prog = Xdp::from_pin("/sys/fs/bpf/aya-xdp-test-prog").unwrap();
let link_id = prog.attach("lo", XdpFlags::default()).unwrap(); let link_id = prog.attach("lo", XdpFlags::default()).unwrap();
let link = prog.take_link(link_id).unwrap(); let link = prog.take_link(link_id).unwrap();
let fd_link: FdLink = link.try_into().unwrap(); let fd_link: FdLink = link.try_into().unwrap();
fd_link.pin("/sys/fs/bpf/aya-xdp-test-lo").unwrap(); fd_link.pin("/sys/fs/bpf/aya-xdp-test-lo").unwrap();
// Unpin the program. It will stay attached since its links were pinned.
prog.unpin().unwrap();
} }
// should still be loaded since link was pinned // should still be loaded since link was pinned
assert_loaded!("pass", true); assert_loaded!("pass", true);
// 2. Load a new version of the program, unpin link, and atomically replace old program // 4. Load a new version of the program, unpin link, and atomically replace old program
{ {
let mut bpf = Bpf::load(bytes).unwrap(); let mut bpf = Bpf::load(bytes).unwrap();
let prog: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); let prog: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap();

Loading…
Cancel
Save