mirror of
https://github.com/kata-containers/cgroups-rs.git
synced 2026-08-05 02:13:23 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e273cd2fb | ||
|
|
bce4b0bb3b | ||
|
|
f5a67c632e | ||
|
|
c8febbc67d | ||
|
|
2536e33696 | ||
|
|
694627e2fc |
2
.github/workflows/bvt.yaml
vendored
2
.github/workflows/bvt.yaml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: BVT
|
name: BVT
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
env:
|
env:
|
||||||
RUST_VERSION: 1.69.0
|
RUST_VERSION: 1.85.1
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = "1.1"
|
|
||||||
nix = { version = "0.25.0", default-features = false, features = ["event", "fs", "process"] }
|
nix = { version = "0.25.0", default-features = false, features = ["event", "fs", "process"] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
|||||||
11
src/blkio.rs
11
src/blkio.rs
@@ -181,13 +181,14 @@ fn parse_io_stat(s: String) -> Vec<IoStat> {
|
|||||||
|
|
||||||
fn parse_io_service_total(s: String) -> Result<u64> {
|
fn parse_io_service_total(s: String) -> Result<u64> {
|
||||||
s.lines()
|
s.lines()
|
||||||
.filter(|x| x.split_whitespace().count() == 2)
|
.find_map(|line| {
|
||||||
.fold(Err(Error::new(ParseError)), |_, x| {
|
let mut parts = line.split_whitespace();
|
||||||
match x.split_whitespace().collect::<Vec<_>>().as_slice() {
|
match (parts.next(), parts.next(), parts.next()) {
|
||||||
["Total", val] => val.parse::<u64>().map_err(|_| Error::new(ParseError)),
|
(Some("Total"), Some(val), None) => val.parse::<u64>().ok(),
|
||||||
_ => Err(Error::new(ParseError)),
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.ok_or_else(|| Error::new(ParseError))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_blkio_data(s: String) -> Result<Vec<BlkIoData>> {
|
fn parse_blkio_data(s: String) -> Result<Vec<BlkIoData>> {
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ mod tests {
|
|||||||
"1,2,3,4".to_string(),
|
"1,2,3,4".to_string(),
|
||||||
"1-5,6-7,8-9".to_string(),
|
"1-5,6-7,8-9".to_string(),
|
||||||
];
|
];
|
||||||
let expecteds = vec![
|
let expecteds = [
|
||||||
vec![(1, 1), (2, 2), (4, 6), (9, 9)],
|
vec![(1, 1), (2, 2), (4, 6), (9, 9)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![(1, 1)],
|
vec![(1, 1)],
|
||||||
|
|||||||
@@ -295,43 +295,48 @@ impl DevicesController {
|
|||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let res = file.read_to_string(&mut s);
|
let res = file.read_to_string(&mut s);
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => {
|
Ok(_) => s
|
||||||
s.lines().fold(Ok(Vec::new()), |acc, line| {
|
.lines()
|
||||||
let ls = line.split(|c| c == ' ' || c == ':').map(|x| x.to_string()).collect::<Vec<String>>();
|
.map(|line| parse_device_line(line, true))
|
||||||
if acc.is_err() || ls.len() != 4 {
|
.collect(),
|
||||||
error!("allowed_devices: acc: {:?}, ls: {:?}", acc, ls);
|
|
||||||
Err(Error::new(ParseError))
|
|
||||||
} else {
|
|
||||||
let devtype = DeviceType::from_char(ls[0].chars().next());
|
|
||||||
let mut major = ls[1].parse::<i64>();
|
|
||||||
let mut minor = ls[2].parse::<i64>();
|
|
||||||
if major.is_err() && ls[1] == "*" {
|
|
||||||
major = Ok(-1);
|
|
||||||
}
|
|
||||||
if minor.is_err() && ls[2] == "*" {
|
|
||||||
minor = Ok(-1);
|
|
||||||
}
|
|
||||||
if devtype.is_none() || major.is_err() || minor.is_err() || !DevicePermissions::is_valid(&ls[3]) {
|
|
||||||
error!("allowed_devices: acc: {:?}, ls: {:?}, devtype: {:?}, major {:?} minor {:?} ls3 {:?}",
|
|
||||||
acc, ls, devtype, major, minor, &ls[3]);
|
|
||||||
Err(Error::new(ParseError))
|
|
||||||
} else {
|
|
||||||
let access = DevicePermissions::from_str(&ls[3])?;
|
|
||||||
let mut acc = acc.unwrap();
|
|
||||||
acc.push(DeviceResource {
|
|
||||||
allow: true,
|
|
||||||
devtype: devtype.unwrap(),
|
|
||||||
major: major.unwrap(),
|
|
||||||
minor: minor.unwrap(),
|
|
||||||
access,
|
|
||||||
});
|
|
||||||
Ok(acc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Err(e) => Err(Error::with_cause(ReadFailed("devices.list".to_string()), e)),
|
Err(e) => Err(Error::with_cause(ReadFailed("devices.list".to_string()), e)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_device_number(s: &str) -> Result<i64> {
|
||||||
|
if s == "*" {
|
||||||
|
Ok(-1)
|
||||||
|
} else {
|
||||||
|
s.parse::<i64>().map_err(|_| Error::new(ParseError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_device_line(line: &str, allow: bool) -> Result<DeviceResource> {
|
||||||
|
let parts: Vec<&str> = line.split([' ', ':']).collect();
|
||||||
|
if parts.len() != 4 {
|
||||||
|
error!("allowed_devices: invalid line format: {:?}", line);
|
||||||
|
return Err(Error::new(ParseError));
|
||||||
|
}
|
||||||
|
|
||||||
|
let devtype = DeviceType::from_char(parts[0].chars().next()).ok_or_else(|| {
|
||||||
|
error!("allowed_devices: invalid device type: {:?}", parts[0]);
|
||||||
|
Error::new(ParseError)
|
||||||
|
})?;
|
||||||
|
let major = parse_device_number(parts[1]).inspect_err(|_| {
|
||||||
|
error!("allowed_devices: invalid major number: {:?}", parts[1]);
|
||||||
|
})?;
|
||||||
|
let minor = parse_device_number(parts[2]).inspect_err(|_| {
|
||||||
|
error!("allowed_devices: invalid minor number: {:?}", parts[2]);
|
||||||
|
})?;
|
||||||
|
let access = DevicePermissions::from_str(parts[3])?;
|
||||||
|
|
||||||
|
Ok(DeviceResource {
|
||||||
|
allow,
|
||||||
|
devtype,
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
access,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
113
src/hugetlb.rs
113
src/hugetlb.rs
@@ -181,7 +181,6 @@ impl HugeTlbController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const HUGEPAGESIZE_DIR: &str = "/sys/kernel/mm/hugepages";
|
pub const HUGEPAGESIZE_DIR: &str = "/sys/kernel/mm/hugepages";
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
@@ -263,37 +262,46 @@ pub fn get_decimal_abbrs() -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_size(s: &str, m: &HashMap<String, u128>) -> Result<u128> {
|
fn parse_size(s: &str, m: &HashMap<String, u128>) -> Result<u128> {
|
||||||
let re = Regex::new(r"(?P<num>\d+)(?P<mul>[kKmMgGtTpP]?)[bB]?$");
|
// Remove leading/trailing whitespace.
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
if re.is_err() {
|
// Remove an optional trailing 'b' or 'B'
|
||||||
|
let s = if let Some(stripped) = s.strip_suffix('b').or_else(|| s.strip_suffix('B')) {
|
||||||
|
stripped
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that the string is not empty after stripping.
|
||||||
|
if s.is_empty() {
|
||||||
return Err(Error::new(InvalidBytesSize));
|
return Err(Error::new(InvalidBytesSize));
|
||||||
}
|
}
|
||||||
let caps = re.unwrap().captures(s).unwrap();
|
|
||||||
|
|
||||||
let num = caps.name("num");
|
// The last character should be the multiplier letter.
|
||||||
let size: u128 = if let Some(num) = num {
|
let last_char = s.chars().last().unwrap();
|
||||||
let n = num.as_str().trim().parse::<u128>();
|
if !"kKmMgGtTpP".contains(last_char) {
|
||||||
if n.is_err() {
|
|
||||||
return Err(Error::new(InvalidBytesSize));
|
|
||||||
}
|
|
||||||
n.unwrap()
|
|
||||||
} else {
|
|
||||||
return Err(Error::new(InvalidBytesSize));
|
return Err(Error::new(InvalidBytesSize));
|
||||||
};
|
}
|
||||||
|
|
||||||
let q = caps.name("mul");
|
// The numeric part is everything before the multiplier letter.
|
||||||
let mul: u128 = if let Some(q) = q {
|
let num_part = &s[..s.len() - last_char.len_utf8()];
|
||||||
let t = m.get(q.as_str());
|
if num_part.trim().is_empty() {
|
||||||
if let Some(t) = t {
|
|
||||||
*t
|
|
||||||
} else {
|
|
||||||
return Err(Error::new(InvalidBytesSize));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::new(InvalidBytesSize));
|
return Err(Error::new(InvalidBytesSize));
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(size * mul)
|
// Parse the numeric part into a u128.
|
||||||
|
let number: u128 = num_part
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| Error::new(InvalidBytesSize))?;
|
||||||
|
|
||||||
|
// Look up the multiplier in the provided HashMap.
|
||||||
|
let multiplier_key = last_char.to_string();
|
||||||
|
let multiplier = m
|
||||||
|
.get(&multiplier_key)
|
||||||
|
.ok_or_else(|| Error::new(InvalidBytesSize))?;
|
||||||
|
|
||||||
|
Ok(number * multiplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn custom_size(mut size: f64, base: f64, m: &[String]) -> String {
|
fn custom_size(mut size: f64, base: f64, m: &[String]) -> String {
|
||||||
@@ -305,3 +313,60 @@ fn custom_size(mut size: f64, base: f64, m: &[String]) -> String {
|
|||||||
|
|
||||||
format!("{}{}", size, m[i].as_str())
|
format!("{}{}", size, m[i].as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binary_size_valid() {
|
||||||
|
let m = get_binary_size_map();
|
||||||
|
// Valid inputs must include a multiplier letter.
|
||||||
|
assert_eq!(parse_size("1k", &m).unwrap(), KiB);
|
||||||
|
assert_eq!(parse_size("2m", &m).unwrap(), 2 * MiB);
|
||||||
|
assert_eq!(parse_size("3g", &m).unwrap(), 3 * GiB);
|
||||||
|
assert_eq!(parse_size("4t", &m).unwrap(), 4 * TiB);
|
||||||
|
assert_eq!(parse_size("5p", &m).unwrap(), 5 * PiB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decimal_size_valid() {
|
||||||
|
let m = get_decimal_size_map();
|
||||||
|
assert_eq!(parse_size("1k", &m).unwrap(), KB);
|
||||||
|
assert_eq!(parse_size("2m", &m).unwrap(), 2 * MB);
|
||||||
|
assert_eq!(parse_size("3g", &m).unwrap(), 3 * GB);
|
||||||
|
assert_eq!(parse_size("4t", &m).unwrap(), 4 * TB);
|
||||||
|
assert_eq!(parse_size("5p", &m).unwrap(), 5 * PB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_b_suffix() {
|
||||||
|
let m = get_binary_size_map();
|
||||||
|
// Trailing 'b' or 'B' should be accepted.
|
||||||
|
assert_eq!(parse_size("1kb", &m).unwrap(), KiB);
|
||||||
|
assert_eq!(parse_size("2mB", &m).unwrap(), 2 * MiB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_inputs() {
|
||||||
|
let m = get_binary_size_map();
|
||||||
|
// Missing multiplier letter results in error.
|
||||||
|
assert!(parse_size("1", &m).is_err());
|
||||||
|
// Invalid multiplier letter.
|
||||||
|
assert!(parse_size("10x", &m).is_err());
|
||||||
|
// Non-numeric input.
|
||||||
|
assert!(parse_size("abc", &m).is_err());
|
||||||
|
// Only multiplier letter with no number.
|
||||||
|
assert!(parse_size("k", &m).is_err());
|
||||||
|
// Number with an invalid trailing character.
|
||||||
|
assert!(parse_size("123z", &m).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uppercase_multiplier_fails() {
|
||||||
|
let m = get_binary_size_map();
|
||||||
|
// Although the regex matches uppercase letters, the provided map only contains lowercase keys.
|
||||||
|
// Therefore, "1K" does not match any key and should produce an error.
|
||||||
|
assert!(parse_size("1K", &m).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ impl From<u64> for CgroupPid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a std::process::Child> for CgroupPid {
|
impl From<&std::process::Child> for CgroupPid {
|
||||||
fn from(u: &std::process::Child) -> CgroupPid {
|
fn from(u: &std::process::Child) -> CgroupPid {
|
||||||
CgroupPid { pid: u.id() as u64 }
|
CgroupPid { pid: u.id() as u64 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,9 +142,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
let hier_unevict_line = ls.next().unwrap_or_default();
|
let hier_unevict_line = ls.next().unwrap_or_default();
|
||||||
|
|
||||||
Ok(NumaStat {
|
Ok(NumaStat {
|
||||||
total_pages: total_line
|
total_pages: total_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
total_pages_per_node: {
|
total_pages_per_node: {
|
||||||
@@ -157,9 +155,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
},
|
},
|
||||||
file_pages: file_line
|
file_pages: file_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
file_pages_per_node: {
|
file_pages_per_node: {
|
||||||
@@ -172,9 +168,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
},
|
},
|
||||||
anon_pages: anon_line
|
anon_pages: anon_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
anon_pages_per_node: {
|
anon_pages_per_node: {
|
||||||
@@ -187,9 +181,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
},
|
},
|
||||||
unevictable_pages: unevict_line
|
unevictable_pages: unevict_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
unevictable_pages_per_node: {
|
unevictable_pages_per_node: {
|
||||||
@@ -204,9 +196,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
},
|
},
|
||||||
hierarchical_total_pages: {
|
hierarchical_total_pages: {
|
||||||
if !hier_total_line.is_empty() {
|
if !hier_total_line.is_empty() {
|
||||||
hier_total_line
|
hier_total_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
@@ -229,9 +219,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
},
|
},
|
||||||
hierarchical_file_pages: {
|
hierarchical_file_pages: {
|
||||||
if !hier_file_line.is_empty() {
|
if !hier_file_line.is_empty() {
|
||||||
hier_file_line
|
hier_file_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
@@ -254,9 +242,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
},
|
},
|
||||||
hierarchical_anon_pages: {
|
hierarchical_anon_pages: {
|
||||||
if !hier_anon_line.is_empty() {
|
if !hier_anon_line.is_empty() {
|
||||||
hier_anon_line
|
hier_anon_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
@@ -279,9 +265,7 @@ fn parse_numa_stat(s: String) -> Result<NumaStat> {
|
|||||||
},
|
},
|
||||||
hierarchical_unevictable_pages: {
|
hierarchical_unevictable_pages: {
|
||||||
if !hier_unevict_line.is_empty() {
|
if !hier_unevict_line.is_empty() {
|
||||||
hier_unevict_line
|
hier_unevict_line.split([' ', '=']).collect::<Vec<_>>()[1]
|
||||||
.split(|x| x == ' ' || x == '=')
|
|
||||||
.collect::<Vec<_>>()[1]
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,36 +94,26 @@ impl NetPrioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A map of priorities for each network interface.
|
/// A map of priorities for each network interface.
|
||||||
#[allow(clippy::iter_nth_zero, clippy::unnecessary_unwrap)]
|
|
||||||
pub fn ifpriomap(&self) -> Result<HashMap<String, u64>> {
|
pub fn ifpriomap(&self) -> Result<HashMap<String, u64>> {
|
||||||
self.open_path("net_prio.ifpriomap", false)
|
self.open_path("net_prio.ifpriomap", false)
|
||||||
.and_then(|file| {
|
.and_then(|file| {
|
||||||
let bf = BufReader::new(file);
|
let bf = BufReader::new(file);
|
||||||
bf.lines().fold(Ok(HashMap::new()), |acc, line| {
|
bf.lines()
|
||||||
if acc.is_err() {
|
.map(|line| {
|
||||||
acc
|
let line = line.map_err(|_| Error::new(ParseError))?;
|
||||||
} else {
|
let mut parts = line.split_whitespace();
|
||||||
let mut acc = acc.unwrap();
|
|
||||||
let l = line.unwrap();
|
|
||||||
let mut sp = l.split_whitespace();
|
|
||||||
|
|
||||||
let ifname = sp.nth(0);
|
let ifname = parts.next().ok_or(Error::new(ParseError))?;
|
||||||
let ifprio = sp.nth(1);
|
let ifprio_str = parts.next().ok_or(Error::new(ParseError))?;
|
||||||
if ifname.is_none() || ifprio.is_none() {
|
|
||||||
Err(Error::new(ParseError))
|
let ifprio = ifprio_str
|
||||||
} else {
|
.trim()
|
||||||
let ifname = ifname.unwrap();
|
.parse()
|
||||||
let ifprio = ifprio.unwrap().trim().parse();
|
.map_err(|e| Error::with_cause(ParseError, e))?;
|
||||||
match ifprio {
|
|
||||||
Err(e) => Err(Error::with_cause(ParseError, e)),
|
Ok((ifname.to_string(), ifprio))
|
||||||
Ok(_) => {
|
})
|
||||||
acc.insert(ifname.to_string(), ifprio.unwrap());
|
.collect::<Result<HashMap<String, _>>>()
|
||||||
Ok(acc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user