2017-04-10 15:06:15 +00:00
|
|
|
/*
|
|
|
|
* virsh-util.c: helpers for virsh
|
|
|
|
*
|
|
|
|
* 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, see
|
|
|
|
* <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <config.h>
|
|
|
|
|
|
|
|
#include "virsh-util.h"
|
|
|
|
|
|
|
|
#include "virfile.h"
|
2017-04-11 08:18:06 +00:00
|
|
|
#include "virstring.h"
|
2021-01-06 11:56:11 +00:00
|
|
|
#include "virxml.h"
|
2017-04-11 08:18:06 +00:00
|
|
|
|
|
|
|
static virDomainPtr
|
|
|
|
virshLookupDomainInternal(vshControl *ctl,
|
|
|
|
const char *cmdname,
|
|
|
|
const char *name,
|
|
|
|
unsigned int flags)
|
|
|
|
{
|
|
|
|
virDomainPtr dom = NULL;
|
|
|
|
int id;
|
2021-03-11 07:16:13 +00:00
|
|
|
virshControl *priv = ctl->privData;
|
2017-04-11 08:18:06 +00:00
|
|
|
|
2020-08-03 15:27:58 +00:00
|
|
|
virCheckFlags(VIRSH_BYID | VIRSH_BYUUID | VIRSH_BYNAME, NULL);
|
|
|
|
|
2017-04-11 08:18:06 +00:00
|
|
|
/* try it by ID */
|
|
|
|
if (flags & VIRSH_BYID) {
|
|
|
|
if (virStrToLong_i(name, NULL, 10, &id) == 0 && id >= 0) {
|
|
|
|
vshDebug(ctl, VSH_ERR_DEBUG, "%s: <domain> looks like ID\n",
|
|
|
|
cmdname);
|
|
|
|
dom = virDomainLookupByID(priv->conn, id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* try it by UUID */
|
|
|
|
if (!dom && (flags & VIRSH_BYUUID) &&
|
|
|
|
strlen(name) == VIR_UUID_STRING_BUFLEN-1) {
|
|
|
|
vshDebug(ctl, VSH_ERR_DEBUG, "%s: <domain> trying as domain UUID\n",
|
|
|
|
cmdname);
|
|
|
|
dom = virDomainLookupByUUIDString(priv->conn, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* try it by NAME */
|
|
|
|
if (!dom && (flags & VIRSH_BYNAME)) {
|
|
|
|
vshDebug(ctl, VSH_ERR_DEBUG, "%s: <domain> trying as domain NAME\n",
|
|
|
|
cmdname);
|
|
|
|
dom = virDomainLookupByName(priv->conn, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
vshResetLibvirtError();
|
|
|
|
|
|
|
|
if (!dom)
|
|
|
|
vshError(ctl, _("failed to get domain '%s'"), name);
|
|
|
|
|
|
|
|
return dom;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
virDomainPtr
|
|
|
|
virshLookupDomainBy(vshControl *ctl,
|
|
|
|
const char *name,
|
|
|
|
unsigned int flags)
|
|
|
|
{
|
|
|
|
return virshLookupDomainInternal(ctl, "unknown", name, flags);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
virDomainPtr
|
|
|
|
virshCommandOptDomainBy(vshControl *ctl,
|
|
|
|
const vshCmd *cmd,
|
|
|
|
const char **name,
|
|
|
|
unsigned int flags)
|
|
|
|
{
|
|
|
|
const char *n = NULL;
|
|
|
|
const char *optname = "domain";
|
|
|
|
|
|
|
|
if (vshCommandOptStringReq(ctl, cmd, optname, &n) < 0)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
vshDebug(ctl, VSH_ERR_INFO, "%s: found option <%s>: %s\n",
|
|
|
|
cmd->def->name, optname, n);
|
|
|
|
|
|
|
|
if (name)
|
|
|
|
*name = n;
|
|
|
|
|
|
|
|
return virshLookupDomainInternal(ctl, cmd->def->name, n, flags);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
virDomainPtr
|
|
|
|
virshCommandOptDomain(vshControl *ctl,
|
|
|
|
const vshCmd *cmd,
|
|
|
|
const char **name)
|
|
|
|
{
|
|
|
|
return virshCommandOptDomainBy(ctl, cmd, name,
|
|
|
|
VIRSH_BYID | VIRSH_BYUUID | VIRSH_BYNAME);
|
|
|
|
}
|
|
|
|
|
2017-04-10 15:06:15 +00:00
|
|
|
|
|
|
|
int
|
|
|
|
virshDomainState(vshControl *ctl,
|
|
|
|
virDomainPtr dom,
|
|
|
|
int *reason)
|
|
|
|
{
|
|
|
|
virDomainInfo info;
|
2021-03-11 07:16:13 +00:00
|
|
|
virshControl *priv = ctl->privData;
|
2017-04-10 15:06:15 +00:00
|
|
|
|
|
|
|
if (reason)
|
|
|
|
*reason = -1;
|
|
|
|
|
|
|
|
if (!priv->useGetInfo) {
|
|
|
|
int state;
|
|
|
|
if (virDomainGetState(dom, &state, reason, 0) < 0) {
|
2018-05-05 12:04:21 +00:00
|
|
|
if (virGetLastErrorCode() == VIR_ERR_NO_SUPPORT)
|
2017-04-10 15:06:15 +00:00
|
|
|
priv->useGetInfo = true;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* fall back to virDomainGetInfo if virDomainGetState is not supported */
|
|
|
|
if (virDomainGetInfo(dom, &info) < 0)
|
|
|
|
return -1;
|
2021-09-23 23:49:11 +00:00
|
|
|
return info.state;
|
2017-04-10 15:06:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int
|
2019-10-14 12:44:29 +00:00
|
|
|
virshStreamSink(virStreamPtr st G_GNUC_UNUSED,
|
2017-04-10 15:06:15 +00:00
|
|
|
const char *bytes,
|
|
|
|
size_t nbytes,
|
|
|
|
void *opaque)
|
|
|
|
{
|
2021-03-11 07:16:13 +00:00
|
|
|
virshStreamCallbackData *cbData = opaque;
|
2017-04-10 15:06:15 +00:00
|
|
|
|
2020-07-02 06:42:44 +00:00
|
|
|
return safewrite(cbData->fd, bytes, nbytes);
|
2017-04-10 15:06:15 +00:00
|
|
|
}
|
2017-04-11 10:16:52 +00:00
|
|
|
|
|
|
|
|
2016-04-27 12:21:10 +00:00
|
|
|
int
|
2019-10-14 12:44:29 +00:00
|
|
|
virshStreamSource(virStreamPtr st G_GNUC_UNUSED,
|
2016-04-27 12:21:10 +00:00
|
|
|
char *bytes,
|
|
|
|
size_t nbytes,
|
|
|
|
void *opaque)
|
|
|
|
{
|
2021-03-11 07:16:13 +00:00
|
|
|
virshStreamCallbackData *cbData = opaque;
|
2016-04-27 12:21:10 +00:00
|
|
|
int fd = cbData->fd;
|
|
|
|
|
|
|
|
return saferead(fd, bytes, nbytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int
|
2019-10-14 12:44:29 +00:00
|
|
|
virshStreamSourceSkip(virStreamPtr st G_GNUC_UNUSED,
|
2016-04-27 12:21:10 +00:00
|
|
|
long long offset,
|
|
|
|
void *opaque)
|
|
|
|
{
|
2021-03-11 07:16:13 +00:00
|
|
|
virshStreamCallbackData *cbData = opaque;
|
2016-04-27 12:21:10 +00:00
|
|
|
int fd = cbData->fd;
|
|
|
|
|
2020-09-23 17:57:09 +00:00
|
|
|
if (lseek(fd, offset, SEEK_CUR) == (off_t) -1)
|
2016-04-27 12:21:10 +00:00
|
|
|
return -1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-12 13:35:04 +00:00
|
|
|
int
|
2019-10-14 12:44:29 +00:00
|
|
|
virshStreamSkip(virStreamPtr st G_GNUC_UNUSED,
|
2016-04-12 13:35:04 +00:00
|
|
|
long long offset,
|
|
|
|
void *opaque)
|
|
|
|
{
|
2021-03-11 07:16:13 +00:00
|
|
|
virshStreamCallbackData *cbData = opaque;
|
2016-04-12 13:35:04 +00:00
|
|
|
off_t cur;
|
|
|
|
|
2020-07-02 13:49:33 +00:00
|
|
|
if (cbData->isBlock) {
|
|
|
|
g_autofree char * buf = NULL;
|
|
|
|
const size_t buflen = 1 * 1024 * 1024; /* 1MiB */
|
2016-04-12 13:35:04 +00:00
|
|
|
|
2020-07-02 13:49:33 +00:00
|
|
|
/* While for files it's enough to lseek() and ftruncate() to create
|
|
|
|
* a hole which would emulate zeroes on read(), for block devices
|
|
|
|
* we have to write zeroes to read() zeroes. And we have to write
|
|
|
|
* @got bytes of zeroes. Do that in smaller chunks though.*/
|
|
|
|
|
|
|
|
buf = g_new0(char, buflen);
|
|
|
|
|
|
|
|
while (offset) {
|
|
|
|
size_t count = MIN(offset, buflen);
|
|
|
|
ssize_t r;
|
|
|
|
|
|
|
|
if ((r = safewrite(cbData->fd, buf, count)) < 0)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
offset -= r;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ((cur = lseek(cbData->fd, offset, SEEK_CUR)) == (off_t) -1)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
if (ftruncate(cbData->fd, cur) < 0)
|
|
|
|
return -1;
|
|
|
|
}
|
2016-04-12 13:35:04 +00:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-27 12:21:10 +00:00
|
|
|
int
|
2019-10-14 12:44:29 +00:00
|
|
|
virshStreamInData(virStreamPtr st G_GNUC_UNUSED,
|
2016-04-27 12:21:10 +00:00
|
|
|
int *inData,
|
|
|
|
long long *offset,
|
|
|
|
void *opaque)
|
|
|
|
{
|
2021-03-11 07:16:13 +00:00
|
|
|
virshStreamCallbackData *cbData = opaque;
|
2016-04-27 12:21:10 +00:00
|
|
|
vshControl *ctl = cbData->ctl;
|
|
|
|
int fd = cbData->fd;
|
|
|
|
|
2020-07-02 13:51:20 +00:00
|
|
|
if (cbData->isBlock) {
|
|
|
|
/* Block devices are always in data section by definition. The
|
|
|
|
* @sectionLen is slightly more tricky. While we could try and get
|
|
|
|
* how much bytes is there left until EOF, we can pretend there is
|
|
|
|
* always X bytes left and let the saferead() below hit EOF (which
|
|
|
|
* is then handled gracefully anyway). Worst case scenario, this
|
|
|
|
* branch is called more than once.
|
|
|
|
* X was chosen to be 1MiB but it has ho special meaning. */
|
|
|
|
*inData = 1;
|
|
|
|
*offset = 1 * 1024 * 1024;
|
|
|
|
} else {
|
|
|
|
if (virFileInData(fd, inData, offset) < 0) {
|
|
|
|
vshError(ctl, "%s", _("Unable to get current position in stream"));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
2016-04-27 12:21:10 +00:00
|
|
|
|
2020-07-02 13:51:20 +00:00
|
|
|
return 0;
|
2016-04-27 12:21:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-11 10:16:52 +00:00
|
|
|
void
|
|
|
|
virshDomainFree(virDomainPtr dom)
|
|
|
|
{
|
|
|
|
if (!dom)
|
|
|
|
return;
|
|
|
|
|
2017-04-11 15:23:23 +00:00
|
|
|
vshSaveLibvirtHelperError();
|
2017-04-11 10:16:52 +00:00
|
|
|
virDomainFree(dom); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
2017-04-11 15:21:05 +00:00
|
|
|
|
|
|
|
|
2019-03-13 21:04:51 +00:00
|
|
|
void
|
|
|
|
virshDomainCheckpointFree(virDomainCheckpointPtr chk)
|
|
|
|
{
|
|
|
|
if (!chk)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virDomainCheckpointFree(chk); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-11 15:21:05 +00:00
|
|
|
void
|
|
|
|
virshDomainSnapshotFree(virDomainSnapshotPtr snap)
|
|
|
|
{
|
|
|
|
if (!snap)
|
|
|
|
return;
|
|
|
|
|
2017-04-11 15:23:23 +00:00
|
|
|
vshSaveLibvirtHelperError();
|
2017-04-11 15:21:05 +00:00
|
|
|
virDomainSnapshotFree(snap); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
2017-04-11 14:51:32 +00:00
|
|
|
|
|
|
|
|
2021-09-26 06:20:54 +00:00
|
|
|
void
|
|
|
|
virshInterfaceFree(virInterfacePtr iface)
|
|
|
|
{
|
|
|
|
if (!iface)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virInterfaceFree(iface); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 08:57:38 +00:00
|
|
|
void
|
|
|
|
virshNetworkFree(virNetworkPtr network)
|
|
|
|
{
|
|
|
|
if (!network)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virNetworkFree(network); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 09:44:30 +00:00
|
|
|
void
|
|
|
|
virshNodeDeviceFree(virNodeDevicePtr device)
|
|
|
|
{
|
|
|
|
if (!device)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virNodeDeviceFree(device); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 11:18:17 +00:00
|
|
|
void
|
|
|
|
virshNWFilterFree(virNWFilterPtr nwfilter)
|
|
|
|
{
|
|
|
|
if (!nwfilter)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virNWFilterFree(nwfilter); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-01-24 14:24:49 +00:00
|
|
|
void
|
|
|
|
virshSecretFree(virSecretPtr secret)
|
|
|
|
{
|
|
|
|
if (!secret)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virSecretFree(secret); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 07:25:41 +00:00
|
|
|
void
|
|
|
|
virshStoragePoolFree(virStoragePoolPtr pool)
|
|
|
|
{
|
|
|
|
if (!pool)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virStoragePoolFree(pool); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 08:28:06 +00:00
|
|
|
void
|
|
|
|
virshStorageVolFree(virStorageVolPtr vol)
|
|
|
|
{
|
|
|
|
if (!vol)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virStorageVolFree(vol); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-26 11:27:26 +00:00
|
|
|
|
|
|
|
void
|
|
|
|
virshStreamFree(virStreamPtr stream)
|
|
|
|
{
|
|
|
|
if (!stream)
|
|
|
|
return;
|
|
|
|
|
|
|
|
vshSaveLibvirtHelperError();
|
|
|
|
virStreamFree(stream); /* sc_prohibit_obj_free_apis_in_virsh */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-11 14:51:32 +00:00
|
|
|
int
|
|
|
|
virshDomainGetXMLFromDom(vshControl *ctl,
|
|
|
|
virDomainPtr dom,
|
|
|
|
unsigned int flags,
|
|
|
|
xmlDocPtr *xml,
|
|
|
|
xmlXPathContextPtr *ctxt)
|
|
|
|
{
|
2021-08-11 13:25:15 +00:00
|
|
|
g_autofree char *desc = NULL;
|
2017-04-11 14:51:32 +00:00
|
|
|
|
|
|
|
if (!(desc = virDomainGetXMLDesc(dom, flags))) {
|
|
|
|
vshError(ctl, _("Failed to get domain description xml"));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
*xml = virXMLParseStringCtxt(desc, _("(domain_definition)"), ctxt);
|
|
|
|
|
|
|
|
if (!(*xml)) {
|
|
|
|
vshError(ctl, _("Failed to parse domain description xml"));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int
|
|
|
|
virshDomainGetXML(vshControl *ctl,
|
|
|
|
const vshCmd *cmd,
|
|
|
|
unsigned int flags,
|
|
|
|
xmlDocPtr *xml,
|
|
|
|
xmlXPathContextPtr *ctxt)
|
|
|
|
{
|
|
|
|
virDomainPtr dom;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
ret = virshDomainGetXMLFromDom(ctl, dom, flags, xml, ctxt);
|
|
|
|
|
|
|
|
virshDomainFree(dom);
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2022-02-21 15:12:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
VIR_ENUM_IMPL(virshDomainBlockJob,
|
|
|
|
VIR_DOMAIN_BLOCK_JOB_TYPE_LAST,
|
|
|
|
N_("Unknown job"),
|
|
|
|
N_("Block Pull"),
|
|
|
|
N_("Block Copy"),
|
|
|
|
N_("Block Commit"),
|
|
|
|
N_("Active Block Commit"),
|
|
|
|
N_("Backup"),
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const char *
|
|
|
|
virshDomainBlockJobToString(int type)
|
|
|
|
{
|
|
|
|
const char *str = virshDomainBlockJobTypeToString(type);
|
|
|
|
return str ? _(str) : _("Unknown job");
|
|
|
|
}
|
2022-06-16 14:59:03 +00:00
|
|
|
|
|
|
|
bool
|
|
|
|
virshDumpXML(vshControl *ctl,
|
|
|
|
const char *xml,
|
|
|
|
const char *url,
|
|
|
|
const char *xpath,
|
|
|
|
bool wrap)
|
|
|
|
{
|
|
|
|
g_autoptr(xmlDoc) doc = NULL;
|
|
|
|
g_autoptr(xmlXPathContext) ctxt = NULL;
|
|
|
|
g_autofree xmlNodePtr *nodes = NULL;
|
|
|
|
int nnodes = 0;
|
|
|
|
size_t i;
|
|
|
|
int oldblanks;
|
|
|
|
|
|
|
|
if (xpath == NULL) {
|
|
|
|
vshPrint(ctl, "%s", xml);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
oldblanks = xmlKeepBlanksDefault(0);
|
|
|
|
doc = virXMLParseString(xml, url);
|
|
|
|
xmlKeepBlanksDefault(oldblanks);
|
|
|
|
if (!doc)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (!(ctxt = virXMLXPathContextNew(doc)))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if ((nnodes = virXPathNodeSet(xpath, ctxt, &nodes)) < 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wrap) {
|
|
|
|
g_autoptr(xmlDoc) newdoc = xmlNewDoc((xmlChar *)"1.0");
|
|
|
|
xmlNodePtr newroot = xmlNewNode(NULL, (xmlChar *)"nodes");
|
|
|
|
g_autofree char *xmlbit = NULL;
|
|
|
|
|
|
|
|
xmlDocSetRootElement(newdoc, newroot);
|
|
|
|
|
|
|
|
for (i = 0; i < nnodes; i++) {
|
|
|
|
g_autoptr(xmlNode) copy = xmlDocCopyNode(nodes[i], newdoc, 1);
|
|
|
|
if (!xmlAddChild(newroot, copy))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
copy = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
xmlbit = virXMLNodeToString(doc, newroot);
|
|
|
|
vshPrint(ctl, "%s\n", xmlbit);
|
|
|
|
} else {
|
|
|
|
for (i = 0; i < nnodes; i++) {
|
|
|
|
g_autofree char *xmlbit = virXMLNodeToString(doc, nodes[i]);
|
|
|
|
vshPrint(ctl, "%s\n", xmlbit);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|