Commit 4295c5de by Serge Hallyn Committed by Stéphane Graber

lxc-destroy: remove btrfs subvolumes

Doing this requires some btrfs functions from bdev to be used in utils.c Because utils.h is imported by lxc_init.c, I had to create a new initutils.[ch] which are used by both lxc_init.c and utils.c We could instead put the btrfs functions into utils.c, which would be a shorter patch, but it really doesn't belong there. So I went the other way figuring there may be more such cases coming up of fns in utils.c needing code from bdev.c which can't go into lxc_init. Currently, if we detect a btrfs subvolume we just remove it. The st_dev on that dir is different, so we cannot detect if this is bound in from another fs easily. If we care, we should check whether this is a mountpoint, this patch doesn't do that. Signed-off-by: 's avatarSerge Hallyn <serge.hallyn@ubuntu.com> Acked-by: 's avatarStéphane Graber <stgraber@ubuntu.com>
parent 0f541e33
......@@ -12,6 +12,7 @@ noinst_HEADERS = \
conf.h \
console.h \
error.h \
initutils.h \
list.h \
log.h \
lxc.h \
......@@ -67,6 +68,7 @@ liblxc_so_SOURCES = \
cgfs.c \
cgroup.c cgroup.h \
lxc.h \
initutils.c initutils.h \
utils.c utils.h \
sync.c sync.h \
namespace.h namespace.c \
......@@ -239,7 +241,7 @@ lxc_checkpoint_SOURCES = lxc_checkpoint.c
if HAVE_STATIC_LIBCAP
sbin_PROGRAMS += init.lxc.static
init_lxc_static_SOURCES = lxc_init.c error.c log.c utils.c caps.c
init_lxc_static_SOURCES = lxc_init.c error.c log.c initutils.c caps.c
if !HAVE_GETLINE
if HAVE_FGETLN
......
......@@ -1253,7 +1253,7 @@ int btrfs_list_get_path_rootid(int fd, u64 *treeid)
return 0;
}
static bool is_btrfs_fs(const char *path)
bool is_btrfs_fs(const char *path)
{
int fd, ret;
struct btrfs_ioctl_space_args sargs;
......@@ -1531,7 +1531,7 @@ static int btrfs_do_destroy_subvol(const char *path)
fd = open(newfull, O_RDONLY);
if (fd < 0) {
ERROR("Error opening %s", newfull);
SYSERROR("Error opening %s", newfull);
free(newfull);
return -1;
}
......@@ -1829,6 +1829,13 @@ ignore_search:
return btrfs_do_destroy_subvol(path);
}
bool btrfs_try_remove_subvol(const char *path)
{
if (!btrfs_detect(path))
return false;
return btrfs_recursive_destroy(path) == 0;
}
static int btrfs_destroy(struct bdev *orig)
{
return btrfs_recursive_destroy(orig->src);
......
......@@ -105,30 +105,4 @@ bool attach_block_device(struct lxc_conf *conf);
void detach_block_device(struct lxc_conf *conf);
bool rootfs_is_blockdev(struct lxc_conf *conf);
/* define constants if the kernel/glibc headers don't define them */
#ifndef MS_DIRSYNC
#define MS_DIRSYNC 128
#endif
#ifndef MS_REC
#define MS_REC 16384
#endif
#ifndef MNT_DETACH
#define MNT_DETACH 2
#endif
#ifndef MS_SLAVE
#define MS_SLAVE (1<<19)
#endif
#ifndef MS_RELATIME
#define MS_RELATIME (1 << 21)
#endif
#ifndef MS_STRICTATIME
#define MS_STRICTATIME (1 << 24)
#endif
#endif
/*
* lxc: linux Container library
*
* (C) Copyright IBM Corp. 2007, 2008
*
* Authors:
* Daniel Lezcano <daniel.lezcano at free.fr>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "initutils.h"
#include "log.h"
lxc_log_define(lxc_initutils, lxc);
static int mount_fs(const char *source, const char *target, const char *type)
{
/* the umount may fail */
if (umount(target))
WARN("failed to unmount %s : %s", target, strerror(errno));
if (mount(source, target, type, 0, NULL)) {
ERROR("failed to mount %s : %s", target, strerror(errno));
return -1;
}
DEBUG("'%s' mounted on '%s'", source, target);
return 0;
}
extern void lxc_setup_fs(void)
{
if (mount_fs("proc", "/proc", "proc"))
INFO("failed to remount proc");
/* if we can't mount /dev/shm, continue anyway */
if (mount_fs("shmfs", "/dev/shm", "tmpfs"))
INFO("failed to mount /dev/shm");
/* If we were able to mount /dev/shm, then /dev exists */
/* Sure, but it's read-only per config :) */
if (access("/dev/mqueue", F_OK) && mkdir("/dev/mqueue", 0666)) {
DEBUG("failed to create '/dev/mqueue'");
return;
}
/* continue even without posix message queue support */
if (mount_fs("mqueue", "/dev/mqueue", "mqueue"))
INFO("failed to mount /dev/mqueue");
}
static char *copy_global_config_value(char *p)
{
int len = strlen(p);
char *retbuf;
if (len < 1)
return NULL;
if (p[len-1] == '\n') {
p[len-1] = '\0';
len--;
}
retbuf = malloc(len+1);
if (!retbuf)
return NULL;
strcpy(retbuf, p);
return retbuf;
}
const char *lxc_global_config_value(const char *option_name)
{
static const char * const options[][2] = {
{ "lxc.bdev.lvm.vg", DEFAULT_VG },
{ "lxc.bdev.lvm.thin_pool", DEFAULT_THIN_POOL },
{ "lxc.bdev.zfs.root", DEFAULT_ZFSROOT },
{ "lxc.lxcpath", NULL },
{ "lxc.default_config", NULL },
{ "lxc.cgroup.pattern", NULL },
{ "lxc.cgroup.use", NULL },
{ NULL, NULL },
};
/* placed in the thread local storage pool for non-bionic targets */
#ifdef HAVE_TLS
static __thread const char *values[sizeof(options) / sizeof(options[0])] = { 0 };
#else
static const char *values[sizeof(options) / sizeof(options[0])] = { 0 };
#endif
/* user_config_path is freed as soon as it is used */
char *user_config_path = NULL;
/*
* The following variables are freed at bottom unconditionally.
* So NULL the value if it is to be returned to the caller
*/
char *user_default_config_path = NULL;
char *user_lxc_path = NULL;
char *user_cgroup_pattern = NULL;
if (geteuid() > 0) {
const char *user_home = getenv("HOME");
if (!user_home)
user_home = "/";
user_config_path = malloc(sizeof(char) * (22 + strlen(user_home)));
user_default_config_path = malloc(sizeof(char) * (26 + strlen(user_home)));
user_lxc_path = malloc(sizeof(char) * (19 + strlen(user_home)));
sprintf(user_config_path, "%s/.config/lxc/lxc.conf", user_home);
sprintf(user_default_config_path, "%s/.config/lxc/default.conf", user_home);
sprintf(user_lxc_path, "%s/.local/share/lxc/", user_home);
user_cgroup_pattern = strdup("lxc/%n");
}
else {
user_config_path = strdup(LXC_GLOBAL_CONF);
user_default_config_path = strdup(LXC_DEFAULT_CONFIG);
user_lxc_path = strdup(LXCPATH);
user_cgroup_pattern = strdup(DEFAULT_CGROUP_PATTERN);
}
const char * const (*ptr)[2];
size_t i;
char buf[1024], *p, *p2;
FILE *fin = NULL;
for (i = 0, ptr = options; (*ptr)[0]; ptr++, i++) {
if (!strcmp(option_name, (*ptr)[0]))
break;
}
if (!(*ptr)[0]) {
free(user_config_path);
free(user_default_config_path);
free(user_lxc_path);
free(user_cgroup_pattern);
errno = EINVAL;
return NULL;
}
if (values[i]) {
free(user_config_path);
free(user_default_config_path);
free(user_lxc_path);
free(user_cgroup_pattern);
return values[i];
}
fin = fopen_cloexec(user_config_path, "r");
free(user_config_path);
if (fin) {
while (fgets(buf, 1024, fin)) {
if (buf[0] == '#')
continue;
p = strstr(buf, option_name);
if (!p)
continue;
/* see if there was just white space in front
* of the option name
*/
for (p2 = buf; p2 < p; p2++) {
if (*p2 != ' ' && *p2 != '\t')
break;
}
if (p2 < p)
continue;
p = strchr(p, '=');
if (!p)
continue;
/* see if there was just white space after
* the option name
*/
for (p2 += strlen(option_name); p2 < p; p2++) {
if (*p2 != ' ' && *p2 != '\t')
break;
}
if (p2 < p)
continue;
p++;
while (*p && (*p == ' ' || *p == '\t')) p++;
if (!*p)
continue;
if (strcmp(option_name, "lxc.lxcpath") == 0) {
free(user_lxc_path);
user_lxc_path = copy_global_config_value(p);
remove_trailing_slashes(user_lxc_path);
values[i] = user_lxc_path;
user_lxc_path = NULL;
goto out;
}
values[i] = copy_global_config_value(p);
goto out;
}
}
/* could not find value, use default */
if (strcmp(option_name, "lxc.lxcpath") == 0) {
remove_trailing_slashes(user_lxc_path);
values[i] = user_lxc_path;
user_lxc_path = NULL;
}
else if (strcmp(option_name, "lxc.default_config") == 0) {
values[i] = user_default_config_path;
user_default_config_path = NULL;
}
else if (strcmp(option_name, "lxc.cgroup.pattern") == 0) {
values[i] = user_cgroup_pattern;
user_cgroup_pattern = NULL;
}
else
values[i] = (*ptr)[1];
/* special case: if default value is NULL,
* and there is no config, don't view that
* as an error... */
if (!values[i])
errno = 0;
out:
if (fin)
fclose(fin);
free(user_cgroup_pattern);
free(user_default_config_path);
free(user_lxc_path);
return values[i];
}
extern void remove_trailing_slashes(char *p)
{
int l = strlen(p);
while (--l >= 0 && (p[l] == '/' || p[l] == '\n'))
p[l] = '\0';
}
FILE *fopen_cloexec(const char *path, const char *mode)
{
int open_mode = 0;
int step = 0;
int fd;
int saved_errno = 0;
FILE *ret;
if (!strncmp(mode, "r+", 2)) {
open_mode = O_RDWR;
step = 2;
} else if (!strncmp(mode, "r", 1)) {
open_mode = O_RDONLY;
step = 1;
} else if (!strncmp(mode, "w+", 2)) {
open_mode = O_RDWR | O_TRUNC | O_CREAT;
step = 2;
} else if (!strncmp(mode, "w", 1)) {
open_mode = O_WRONLY | O_TRUNC | O_CREAT;
step = 1;
} else if (!strncmp(mode, "a+", 2)) {
open_mode = O_RDWR | O_CREAT | O_APPEND;
step = 2;
} else if (!strncmp(mode, "a", 1)) {
open_mode = O_WRONLY | O_CREAT | O_APPEND;
step = 1;
}
for (; mode[step]; step++)
if (mode[step] == 'x')
open_mode |= O_EXCL;
open_mode |= O_CLOEXEC;
fd = open(path, open_mode, 0666);
if (fd < 0)
return NULL;
ret = fdopen(fd, mode);
saved_errno = errno;
if (!ret)
close(fd);
errno = saved_errno;
return ret;
}
/*
* lxc: linux Container library
*
* (C) Copyright IBM Corp. 2007, 2008
*
* Authors:
* Daniel Lezcano <daniel.lezcano at free.fr>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef __LXC_INITUTILS_H
#define __LXC_INITUTILS_H
#include <errno.h>
#include <stdio.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include "config.h"
#define DEFAULT_VG "lxc"
#define DEFAULT_THIN_POOL "lxc"
#define DEFAULT_ZFSROOT "lxc"
extern void lxc_setup_fs(void);
extern const char *lxc_global_config_value(const char *option_name);
/* open a file with O_CLOEXEC */
extern void remove_trailing_slashes(char *p);
FILE *fopen_cloexec(const char *path, const char *mode);
#endif /* __LXC_INITUTILS_H */
......@@ -36,7 +36,7 @@
#include "log.h"
#include "caps.h"
#include "error.h"
#include "utils.h"
#include "initutils.h"
lxc_log_define(lxc_init, lxc);
......
......@@ -69,6 +69,11 @@
lxc_log_define(lxc_utils, lxc);
/*
* if path is btrfs, tries to remove it and any subvolumes beneath it
*/
extern bool btrfs_try_remove_subvol(const char *path);
static int _recursive_rmdir(char *dirname, dev_t pdev,
const char *exclude, int level, bool onedev)
{
......@@ -127,11 +132,16 @@ static int _recursive_rmdir(char *dirname, dev_t pdev,
ret = lstat(pathname, &mystat);
if (ret) {
ERROR("%s: failed to stat %s", __func__, pathname);
failed=1;
failed = 1;
continue;
}
if (onedev && mystat.st_dev != pdev)
if (onedev && mystat.st_dev != pdev) {
/* TODO should we be checking /proc/self/mountinfo for
* pathname and not doing this if found? */
if (btrfs_try_remove_subvol(pathname))
INFO("Removed btrfs subvolume at %s\n", pathname);
continue;
}
if (S_ISDIR(mystat.st_mode)) {
if (_recursive_rmdir(pathname, pdev, exclude, level+1, onedev) < 0)
failed=1;
......@@ -143,11 +153,9 @@ static int _recursive_rmdir(char *dirname, dev_t pdev,
}
}
if (rmdir(dirname) < 0) {
if (!hadexclude) {
ERROR("%s: failed to delete %s", __func__, dirname);
failed=1;
}
if (rmdir(dirname) < 0 && !btrfs_try_remove_subvol(dirname) && !hadexclude) {
ERROR("%s: failed to delete %s", __func__, dirname);
failed=1;
}
ret = closedir(dir);
......@@ -196,43 +204,6 @@ extern int lxc_rmdir_onedev(char *path, const char *exclude)
return _recursive_rmdir(path, mystat.st_dev, exclude, 0, onedev);
}
static int mount_fs(const char *source, const char *target, const char *type)
{
/* the umount may fail */
if (umount(target))
WARN("failed to unmount %s : %s", target, strerror(errno));
if (mount(source, target, type, 0, NULL)) {
ERROR("failed to mount %s : %s", target, strerror(errno));
return -1;
}
DEBUG("'%s' mounted on '%s'", source, target);
return 0;
}
extern void lxc_setup_fs(void)
{
if (mount_fs("proc", "/proc", "proc"))
INFO("failed to remount proc");
/* if we can't mount /dev/shm, continue anyway */
if (mount_fs("shmfs", "/dev/shm", "tmpfs"))
INFO("failed to mount /dev/shm");
/* If we were able to mount /dev/shm, then /dev exists */
/* Sure, but it's read-only per config :) */
if (access("/dev/mqueue", F_OK) && mkdir("/dev/mqueue", 0666)) {
DEBUG("failed to create '/dev/mqueue'");
return;
}
/* continue even without posix message queue support */
if (mount_fs("mqueue", "/dev/mqueue", "mqueue"))
INFO("failed to mount /dev/mqueue");
}
/* borrowed from iproute2 */
extern int get_u16(unsigned short *val, const char *arg, int base)
{
......@@ -275,195 +246,6 @@ extern int mkdir_p(const char *dir, mode_t mode)
return 0;
}
extern void remove_trailing_slashes(char *p)
{
int l = strlen(p);
while (--l >= 0 && (p[l] == '/' || p[l] == '\n'))
p[l] = '\0';
}
static char *copy_global_config_value(char *p)
{
int len = strlen(p);
char *retbuf;
if (len < 1)
return NULL;
if (p[len-1] == '\n') {
p[len-1] = '\0';
len--;
}
retbuf = malloc(len+1);
if (!retbuf)
return NULL;
strcpy(retbuf, p);
return retbuf;
}
#define DEFAULT_VG "lxc"
#define DEFAULT_THIN_POOL "lxc"
#define DEFAULT_ZFSROOT "lxc"
const char *lxc_global_config_value(const char *option_name)
{
static const char * const options[][2] = {
{ "lxc.bdev.lvm.vg", DEFAULT_VG },
{ "lxc.bdev.lvm.thin_pool", DEFAULT_THIN_POOL },
{ "lxc.bdev.zfs.root", DEFAULT_ZFSROOT },
{ "lxc.lxcpath", NULL },
{ "lxc.default_config", NULL },
{ "lxc.cgroup.pattern", NULL },
{ "lxc.cgroup.use", NULL },
{ NULL, NULL },
};
/* placed in the thread local storage pool for non-bionic targets */
#ifdef HAVE_TLS
static __thread const char *values[sizeof(options) / sizeof(options[0])] = { 0 };
#else
static const char *values[sizeof(options) / sizeof(options[0])] = { 0 };
#endif
/* user_config_path is freed as soon as it is used */
char *user_config_path = NULL;
/*
* The following variables are freed at bottom unconditionally.
* So NULL the value if it is to be returned to the caller
*/
char *user_default_config_path = NULL;
char *user_lxc_path = NULL;
char *user_cgroup_pattern = NULL;
if (geteuid() > 0) {
const char *user_home = getenv("HOME");
if (!user_home)
user_home = "/";
user_config_path = malloc(sizeof(char) * (22 + strlen(user_home)));
user_default_config_path = malloc(sizeof(char) * (26 + strlen(user_home)));
user_lxc_path = malloc(sizeof(char) * (19 + strlen(user_home)));
sprintf(user_config_path, "%s/.config/lxc/lxc.conf", user_home);
sprintf(user_default_config_path, "%s/.config/lxc/default.conf", user_home);
sprintf(user_lxc_path, "%s/.local/share/lxc/", user_home);
user_cgroup_pattern = strdup("lxc/%n");
}
else {
user_config_path = strdup(LXC_GLOBAL_CONF);
user_default_config_path = strdup(LXC_DEFAULT_CONFIG);
user_lxc_path = strdup(LXCPATH);
user_cgroup_pattern = strdup(DEFAULT_CGROUP_PATTERN);
}
const char * const (*ptr)[2];
size_t i;
char buf[1024], *p, *p2;
FILE *fin = NULL;
for (i = 0, ptr = options; (*ptr)[0]; ptr++, i++) {
if (!strcmp(option_name, (*ptr)[0]))
break;
}
if (!(*ptr)[0]) {
free(user_config_path);
free(user_default_config_path);
free(user_lxc_path);
free(user_cgroup_pattern);
errno = EINVAL;
return NULL;
}
if (values[i]) {
free(user_config_path);
free(user_default_config_path);
free(user_lxc_path);
free(user_cgroup_pattern);
return values[i];
}
fin = fopen_cloexec(user_config_path, "r");
free(user_config_path);
if (fin) {
while (fgets(buf, 1024, fin)) {
if (buf[0] == '#')
continue;
p = strstr(buf, option_name);
if (!p)
continue;
/* see if there was just white space in front
* of the option name
*/
for (p2 = buf; p2 < p; p2++) {
if (*p2 != ' ' && *p2 != '\t')
break;
}
if (p2 < p)
continue;
p = strchr(p, '=');
if (!p)
continue;
/* see if there was just white space after
* the option name
*/
for (p2 += strlen(option_name); p2 < p; p2++) {
if (*p2 != ' ' && *p2 != '\t')
break;
}
if (p2 < p)
continue;
p++;
while (*p && (*p == ' ' || *p == '\t')) p++;
if (!*p)
continue;
if (strcmp(option_name, "lxc.lxcpath") == 0) {
free(user_lxc_path);
user_lxc_path = copy_global_config_value(p);
remove_trailing_slashes(user_lxc_path);
values[i] = user_lxc_path;
user_lxc_path = NULL;
goto out;
}
values[i] = copy_global_config_value(p);
goto out;
}
}
/* could not find value, use default */
if (strcmp(option_name, "lxc.lxcpath") == 0) {
remove_trailing_slashes(user_lxc_path);
values[i] = user_lxc_path;
user_lxc_path = NULL;
}
else if (strcmp(option_name, "lxc.default_config") == 0) {
values[i] = user_default_config_path;
user_default_config_path = NULL;
}
else if (strcmp(option_name, "lxc.cgroup.pattern") == 0) {
values[i] = user_cgroup_pattern;
user_cgroup_pattern = NULL;
}
else
values[i] = (*ptr)[1];
/* special case: if default value is NULL,
* and there is no config, don't view that
* as an error... */
if (!values[i])
errno = 0;
out:
if (fin)
fclose(fin);
free(user_cgroup_pattern);
free(user_default_config_path);
free(user_lxc_path);
return values[i];
}
char *get_rundir()
{
char *rundir;
......@@ -669,50 +451,6 @@ const char** lxc_va_arg_list_to_argv_const(va_list ap, size_t skip)
return (const char**)lxc_va_arg_list_to_argv(ap, skip, 0);
}
FILE *fopen_cloexec(const char *path, const char *mode)
{
int open_mode = 0;
int step = 0;
int fd;
int saved_errno = 0;
FILE *ret;
if (!strncmp(mode, "r+", 2)) {
open_mode = O_RDWR;
step = 2;
} else if (!strncmp(mode, "r", 1)) {
open_mode = O_RDONLY;
step = 1;
} else if (!strncmp(mode, "w+", 2)) {
open_mode = O_RDWR | O_TRUNC | O_CREAT;
step = 2;
} else if (!strncmp(mode, "w", 1)) {
open_mode = O_WRONLY | O_TRUNC | O_CREAT;
step = 1;
} else if (!strncmp(mode, "a+", 2)) {
open_mode = O_RDWR | O_CREAT | O_APPEND;
step = 2;
} else if (!strncmp(mode, "a", 1)) {
open_mode = O_WRONLY | O_CREAT | O_APPEND;
step = 1;
}
for (; mode[step]; step++)
if (mode[step] == 'x')
open_mode |= O_EXCL;
open_mode |= O_CLOEXEC;
fd = open(path, open_mode, 0666);
if (fd < 0)
return NULL;
ret = fdopen(fd, mode);
saved_errno = errno;
if (!ret)
close(fd);
errno = saved_errno;
return ret;
}
extern struct lxc_popen_FILE *lxc_popen(const char *command)
{
struct lxc_popen_FILE *fp = NULL;
......
......@@ -32,17 +32,14 @@
#include <unistd.h>
#include "config.h"
#include "initutils.h"
/* returns 1 on success, 0 if there were any failures */
extern int lxc_rmdir_onedev(char *path, const char *exclude);
extern void lxc_setup_fs(void);
extern int get_u16(unsigned short *val, const char *arg, int base);
extern int mkdir_p(const char *dir, mode_t mode);
extern void remove_trailing_slashes(char *p);
extern char *get_rundir(void);
extern const char *lxc_global_config_value(const char *option_name);
/* Define getline() if missing from the C library */
#ifndef HAVE_GETLINE
#ifdef HAVE_FGETLN
......@@ -148,10 +145,6 @@ static inline int signalfd(int fd, const sigset_t *mask, int flags)
}
#endif
/* open a file with O_CLOEXEC */
FILE *fopen_cloexec(const char *path, const char *mode);
/* Struct to carry child pid from lxc_popen() to lxc_pclose().
* Not an opaque struct to allow direct access to the underlying FILE *
* (i.e., struct lxc_popen_FILE *file; fgets(buf, sizeof(buf), file->f))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment