diff --git a/mycateye-agent/src/main/java/io/mycat/eye/agent/controller/ZkController.java b/mycateye-agent/src/main/java/io/mycat/eye/agent/controller/ZkController.java new file mode 100644 index 0000000000000000000000000000000000000000..10c1d4feaea0a82cce166ab6ff311d461ef4f807 --- /dev/null +++ b/mycateye-agent/src/main/java/io/mycat/eye/agent/controller/ZkController.java @@ -0,0 +1,339 @@ +package io.mycat.eye.agent.controller; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.mycat.eye.agent.bean.Constant; +import io.mycat.eye.agent.dto.RestResponse; +import io.mycat.eye.agent.service.zkConfig.ZkConfigService; +import io.mycat.eye.agent.service.zkConfig.ZkConfigServiceFactory; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +/** + * cjw + */ +@RestController +@RequestMapping("/zk") +public class ZkController { + ZkConfigService userJson; + ZkConfigService indexToCharsetPropertiesJson; + ZkConfigService defaultJson; + ZkConfigService clusterJson; + + public ZkController(ZkConfigServiceFactory zkConfigServiceFactory) { + this.userJson = zkConfigServiceFactory.create("user", "server/user"); + this.indexToCharsetPropertiesJson = zkConfigServiceFactory + .create("index_to_charset_properties", "server/index_to_charset.properties"); + this.defaultJson = zkConfigServiceFactory.create("default", "server/default"); + this.clusterJson = zkConfigServiceFactory.create("cluster", "server/cluster"); + } + + /** + * http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/cluster/all + * + * {"code":200,"message":"SUCCESS","data":["mycat_fz_01"],"timestamp":1531316353239} + * + * @param cluster + * @return + * @throws Exception + */ + @RequestMapping(value = "/{cluster}/server/cluster/all", method = {RequestMethod.GET}) + @CrossOrigin(origins = "*") + public RestResponse getClusterAll(@PathVariable String cluster) throws Exception { + RestResponse restResponse = new RestResponse<>(); + try { + List children = clusterJson.getChildrenInCluster(cluster); + restResponse.setData(children); + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + } catch (Exception e) { + e.printStackTrace(); + restResponse.setCode(Constant.FAIL_CODE); + restResponse.setMessage(Constant.FAIL_MESSAGE); + } + return restResponse; + } + + /** + * http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/cluster/mycat_fz_01 + * + * { + "code": 200, + "message": "SUCCESS", + "data": "{\"property\":[{\"value\":\"1\",\"name\":\"useSqlStat\"},{\"value\":\"0\",\"name\":\"useGlobleTableCheck\"},{\"value\":\"druidparser\",\"name\":\"defaultSqlParser\"},{\"value\":\"2\",\"name\":\"sequnceHandlerType\"},{\"value\":\"0\",\"name\":\"processorBufferPoolType\"},{\"value\":\"0\",\"name\":\"handleDistributedTransactions\"},{\"value\":\"1\",\"name\":\"useOffHeapForMerge\"},{\"value\":\"1m\",\"name\":\"memoryPageSize\"},{\"value\":\"1k\",\"name\":\"spillsFileBufferSize\"},{\"value\":\"0\",\"name\":\"useStreamOutput\"},{\"value\":\"389m\",\"name\":\"systemReserveMemorySize\"}]}", + "timestamp": 1531316847169 + } + * @param cluster + * @param clusterName + * @return + * @throws Exception + */ + @RequestMapping(value = "/{cluster}/server/cluster/{clusterName}", method = {RequestMethod.GET}) + @CrossOrigin(origins = "*") + public RestResponse getCluster(@PathVariable String cluster, @PathVariable String clusterName) throws Exception { + RestResponse restResponse = new RestResponse<>(); + restResponse.setCode(Constant.FAIL_CODE); + restResponse.setMessage(Constant.FAIL_MESSAGE); + final String finalClusterName = clusterName; + try { + Optional children = clusterJson.getChildrenInClusterWithPath(cluster, finalClusterName::equals); + if (children.isPresent()) { + String data = clusterJson.getData(children.get()); + restResponse.setData(data); + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + return restResponse; + } + } catch (Exception e) { + e.printStackTrace(); + } + return restResponse; + } + + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/cluster/mycat_fz_01 + { + "property": [{ + "value": "1", + "name": "useSqlStat" + }, { + "value": "0", + "name": "useGlobleTableCheck" + }, { + "value": "druidparser", + "name": "defaultSqlParser" + }, { + "value": "2", + "name": "sequnceHandlerType" + }, { + "value": "0", + "name": "processorBufferPoolType" + }, { + "value": "0", + "name": "handleDistributedTransactions" + }, { + "value": "1", + "name": "useOffHeapForMerge" + }, { + "value": "1m", + "name": "memoryPageSize" + }, { + "value": "1k", + "name": "spillsFileBufferSize" + }, { + "value": "0", + "name": "useStreamOutput" + }, { + "value": "389m", + "name": "systemReserveMemorySize" + }] + } + + * + * @param cluster + * @param body + * @param clusterName + * @return + */ + @RequestMapping(value = "/{cluster}/server/cluster/{clusterName}", method = {RequestMethod.POST}) + @CrossOrigin(origins = "*") + public RestResponse updateCluster(@PathVariable String cluster, @RequestBody String body, @PathVariable String clusterName) { + RestResponse restResponse = new RestResponse<>(); + restResponse.setMessage(Constant.FAIL_MESSAGE); + restResponse.setCode(Constant.FAIL_CODE); + JSONObject jsonObject = JSON.parseObject(body); + //todo check + final String finalClusterName = clusterName.trim(); + try { + Optional path = clusterJson.getChildrenInClusterWithPath(cluster, finalClusterName::equals); + if (path.isPresent()) { + clusterJson.setData(path.get(),jsonObject.toJSONString()); + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + return restResponse; + } + } catch (Exception e) { + e.printStackTrace(); + } + return restResponse; + } + + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/default + { + "code": 200, + "message": "SUCCESS", + "data": "{\"property\":[{\"value\":\"1\",\"name\":\"useSqlStat\"},{\"value\":\"0\",\"name\":\"useGlobleTableCheck\"},{\"value\":\"druidparser\",\"name\":\"defaultSqlParser\"},{\"value\":\"2\",\"name\":\"sequnceHandlerType\"},{\"value\":\"0\",\"name\":\"processorBufferPoolType\"},{\"value\":\"0\",\"name\":\"handleDistributedTransactions\"},{\"value\":\"1\",\"name\":\"useOffHeapForMerge\"},{\"value\":\"1m\",\"name\":\"memoryPageSize\"},{\"value\":\"1k\",\"name\":\"spillsFileBufferSize\"},{\"value\":\"0\",\"name\":\"useStreamOutput\"},{\"value\":\"384m\",\"name\":\"systemReserveMemorySize\"}]}", + "timestamp": 1531317511346 + } + * @param cluster + * @return + */ + @RequestMapping(value = "/{cluster}/server/default", method = {RequestMethod.GET}) + @CrossOrigin(origins = "*") + public RestResponse getDefault(@PathVariable String cluster) { + return query(defaultJson, cluster); + } + + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/default + { + "property": [{ + "value": "1", + "name": "useSqlStat" + }, { + "value": "0", + "name": "useGlobleTableCheck" + }, { + "value": "druidparser", + "name": "defaultSqlParser" + }, { + "value": "2", + "name": "sequnceHandlerType" + }, { + "value": "0", + "name": "processorBufferPoolType" + }, { + "value": "0", + "name": "handleDistributedTransactions" + }, { + "value": "1", + "name": "useOffHeapForMerge" + }, { + "value": "1m", + "name": "memoryPageSize" + }, { + "value": "1k", + "name": "spillsFileBufferSize" + }, { + "value": "0", + "name": "useStreamOutput" + }, { + "value": "384m", + "name": "systemReserveMemorySize" + }] + } + * @param cluster + * @param body + * @return + */ + @RequestMapping(value = "/{cluster}/server/default", method = {RequestMethod.POST}) + @CrossOrigin(origins = "*") + public RestResponse updateDefault(@PathVariable String cluster, @RequestBody String body) { + JSONObject jsonObject = JSON.parseObject(body); + //todo check + return update(defaultJson, cluster, jsonObject); + } + + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/user + { + "code": 200, + "message": "SUCCESS", + "data": "[{\"name\":\"root\",\"property\":[{\"value\":\"\",\"name\":\"password\"},{\"value\":\"TESTDB\",\"name\":\"schemas\"}]},{\"name\":\"user\",\"property\":[{\"value\":\"\",\"name\":\"password\"},{\"value\":\"TESTDB\",\"name\":\"schemas\"},{\"value\":\"true\",\"name\":\"readOnly\"}]}]", + "timestamp": 1531317729672 + } + * @param cluster + * @return + */ + @RequestMapping(value = "/{cluster}/server/user", method = {RequestMethod.GET}) + @CrossOrigin(origins = "*") + public RestResponse getUser(@PathVariable String cluster) { + return query(userJson, cluster); + } + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/user + [{ + "name": "root", + "property": [{ + "value": "", + "name": "password" + }, { + "value": "TESTDB", + "name": "schemas" + }] + }, { + "name": "user", + "property": [{ + "value": "", + "name": "password" + }, { + "value": "TESTDB", + "name": "schemas" + }, { + "value": "true", + "name": "readOnly" + }] + }] + * @param cluster + * @return + */ + @RequestMapping(value = "/{cluster}/server/user", method = {RequestMethod.POST}) + @CrossOrigin(origins = "*") + public RestResponse updateUser(@PathVariable String cluster, @RequestBody String body) { + JSON jsonObject = JSON.parseArray(body);//这里接收的是json数组 + //todo check + return update(userJson, cluster, jsonObject); + } + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/charset + * @param cluster + * @return + */ + @RequestMapping(value = "/{cluster}/server/charset", method = {RequestMethod.GET}) + @CrossOrigin(origins = "*") + public RestResponse getCharset(@PathVariable String cluster) { + return query(indexToCharsetPropertiesJson, cluster); + } + /** + http://127.0.0.1:7003/agent/zk/mycat-cluster-1/server/charset + * @param cluster + * @return + */ + @RequestMapping(value = "/{cluster}/server/charset", method = {RequestMethod.POST}) + @CrossOrigin(origins = "*") + public RestResponse updateCharset(@PathVariable String cluster, @RequestBody String body) { + RestResponse restResponse = new RestResponse<>(); + restResponse.setCode(Constant.FAIL_CODE); + restResponse.setMessage(Constant.FAIL_MESSAGE); + try { + userJson.updateAsString(cluster, body);//字符串 + //todo check + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + } catch (Exception e) { + e.printStackTrace(); + } + return restResponse; + } + + private RestResponse query(ZkConfigService json, String cluster) { + RestResponse restResponse = new RestResponse<>(); + try { + restResponse.setData(json.getAsString(cluster)); + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + } catch (Exception e) { + e.printStackTrace();/**/ + restResponse.setCode(Constant.FAIL_CODE); + restResponse.setMessage(Constant.FAIL_MESSAGE); + } + return restResponse; + } + + private RestResponse update(ZkConfigService userJson, String cluster, JSON jsonObject) { + RestResponse restResponse = new RestResponse<>(); + try { + userJson.updateAsJson(cluster, jsonObject); + restResponse.setCode(Constant.SUCCESS_CODE); + restResponse.setMessage(Constant.SUCCESS_MESSAGE); + } catch (Exception e) { + e.printStackTrace(); + restResponse.setCode(Constant.FAIL_CODE); + restResponse.setMessage(Constant.FAIL_MESSAGE); + } + return restResponse; + } +} diff --git a/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigService.java b/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7f3eda164c64bcd9ab0d1961e79bd56596fa2e6c --- /dev/null +++ b/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigService.java @@ -0,0 +1,184 @@ +package io.mycat.eye.agent.service.zkConfig; + +import com.alibaba.fastjson.JSON; +import org.apache.curator.framework.CuratorFramework; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class ZkConfigService { + String path; + String serviceName; + CuratorFramework client; + final static String templeate = "/mycat/%s/%s"; + protected Logger logger; + + public void setData(String path,String str)throws Exception{ + client.transactionOp().setData().forPath(path,str.getBytes()); + } + + /** + * 根据绝对路径获取字符串数据 + * @param path + * @return + * @throws Exception + */ + public String getData(String path)throws Exception{ + return new String(client.getData().forPath(path)); + } + + /** + * 根据模板拼接集群路径获取字符串数据 + * @param cluster + * @return + * @throws Exception + */ + public String getDataInCluster(String cluster) throws Exception { + return new String(client.getData().forPath(String.format(templeate, cluster, path))); + } + + /** + * 根据模板拼接集群路径设置字符串数据 + * @param cluster + * @param data + * @throws Exception + */ + public void saveDataInCluster(String cluster, String data) throws Exception { + client.transactionOp().setData().forPath(String.format(templeate, cluster, path), data.getBytes()); + } + + /** + * 根据模板拼接集群路径设置子节点 + * @param cluster + * @return + * @throws Exception + */ + public List getChildrenInCluster(String cluster) throws Exception { + String format = String.format(templeate, cluster, path); + return client.getChildren() + .forPath(format); + } + + /** + * 根据模板拼接集群路径获取带有子节点的路径 + * @param cluster + * @return + * @throws Exception + */ + public List getChildrenInClusterWithPath(String cluster) throws Exception { + String format = String.format(templeate, cluster, path); + return client.getChildren() + .forPath(format) + .stream() + .map(i -> format + "/" + i) + .collect(Collectors.toList()); + } + + /** + * 根据模板拼接集群路径获取唯一的带有子节点的路径,使用predicate过滤 + * @param cluster + * @param predicate + * @return + * @throws Exception + */ + public Optional getChildrenInClusterWithPath(String cluster, Predicate predicate) throws Exception { + String format = String.format(templeate, cluster, path); + return client.getChildren() + .forPath(format) + .stream() + .filter(predicate) + .map(i -> format + "/" + i) + .findFirst(); + } + + /** + * 根据模板拼接集群路径设置子节点 + * @param key + * @param text + * @throws Exception + */ + public void setChildrenInCluster(String key, String text) throws Exception { + String format = String.format(templeate, client, path); + client.transactionOp().setData().forPath(format + "/" + key, text.getBytes()); + } + + /** + * 根据模板拼接集群路径获取唯一的带有不带有路径的子节点,使用predicate过滤 + * @param cluster + * @param predicate + * @return + * @throws Exception + */ + public Optional getChildrenInCluster(String cluster, Predicate predicate) throws Exception { + String format = String.format(templeate, cluster, path); + return client.getChildren() + .forPath(format) + .stream() + .filter(predicate) + .findFirst(); + } + + /** + * 获取该服务的路径 + * @return + */ + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public ZkConfigService(String k, String path, CuratorFramework client) { + this.path = path; + this.serviceName = k; + this.client = client; + this.logger = LoggerFactory.getLogger(serviceName); + } + + /** + * 根据模板拼接集群路径设置json + * @param cluster + * @param json + * @throws Exception + */ + public void updateAsJson(String cluster, JSON json) throws Exception { + this.saveDataInCluster(cluster, json.toJSONString()); + } + /** + * 根据模板拼接集群路径设置字符串 + * @param cluster + * @param str + * @throws Exception + */ + public void updateAsString(String cluster, String str) throws Exception { + this.saveDataInCluster(cluster, str); + } + + /** + * 根据模板拼接集群路径获取字符串 + * @param cluster + * @return + * @throws Exception + */ + public String getAsString(String cluster) throws Exception { + return getDataInCluster(cluster); + } + + public CuratorFramework getClient() { + return client; + } +} diff --git a/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigServiceFactory.java b/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigServiceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..6e8a556e3c19e9a41046d0432f943739c83cf219 --- /dev/null +++ b/mycateye-agent/src/main/java/io/mycat/eye/agent/service/zkConfig/ZkConfigServiceFactory.java @@ -0,0 +1,41 @@ +package io.mycat.eye.agent.service.zkConfig; + +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ZkConfigServiceFactory { + CuratorFramework client; + + @Autowired + public ZkConfigServiceFactory() { + String connectString = "localhost:2181"; + int sessionTimeoutMs = 5000; + //ExponentialBackoffRetry:重试指定的次数, 且每一次重试之间停顿的时间逐渐增加. + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + client = CuratorFrameworkFactory.builder() + .connectString(connectString) + .sessionTimeoutMs(sessionTimeoutMs) + .retryPolicy(retryPolicy) + .build(); + client.start(); + } + + public ZkConfigService create(String k, String path) { + ZkConfigService service = new ZkConfigService(k, path, client); + return service; + } + + public CuratorFramework getClient() { + return client; + } + + public void setClient(CuratorFramework client) { + this.client = client; + } + +} diff --git a/mycateye-web/src/main/java/io/mycat/eye/web/util/AgentUrlUtil.java b/mycateye-web/src/main/java/io/mycat/eye/web/util/AgentUrlUtil.java index 8b232b1cfa49c80379f3b2ce35fad58a96012d1b..bcb764e4890d5a779044890c7b6cd44b5e172a57 100644 --- a/mycateye-web/src/main/java/io/mycat/eye/web/util/AgentUrlUtil.java +++ b/mycateye-web/src/main/java/io/mycat/eye/web/util/AgentUrlUtil.java @@ -4,125 +4,139 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component -public class AgentUrlUtil -{ +public class AgentUrlUtil { @Value("${agent.url}") private String agentUrl; - - public String getMySqlExplain() - { + + public String getMySqlExplain() { return agentUrl + "/get/explain?serverId={serverId}&schema={schema}&sql={sql}"; } - - public String getMysqlServerVerify() - { + + public String getMysqlServerVerify() { return agentUrl + "/mysql/verify/{host}/{port}/{username}/{password}"; } - - public String getOsServerVerify() - { + + public String getOsServerVerify() { return agentUrl + "/ssh/verify/{host}/{port}/{username}/{password}"; } - - public String getStatement() - { + + public String getStatement() { return agentUrl + "/statement/history/{serverId}/{orderBy}"; } - - public String getCreateTable() - { + + String agentZkUrl = agentUrl + "/zk"; + + public String getZkClusterAll() { + return agentZkUrl + "/{cluster}/server/cluster/all"; + } + + public String getZkCluster() { + return agentZkUrl + "/{cluster}/server/cluster/{clusterName}"; + } + + public String updateZkCluster() { + return agentZkUrl + "/{cluster}/server/cluster/{clusterName}"; + } + + public String getDefault() { + return agentZkUrl + "/{cluster}/server/default"; + } + + public String updateDefault() { + return agentZkUrl + "/{cluster}/server/default"; + } + + public String getUser() { + return agentZkUrl + "/{cluster}/server/user"; + } + + public String updateUser() { + return agentZkUrl + "/{cluster}/server/user"; + } + + public String getCharset() { + return agentZkUrl + "/{cluster}/server/charset"; + } + + public String updateCharset() { + return agentZkUrl + "/{cluster}/server/charset"; + } + + public String getCreateTable() { return agentUrl + "/statement/show-create-table/{serverId}/{schema}/{table}"; } - - public String getTableIndex() - { + + public String getTableIndex() { return agentUrl + "/statement/show-index-from-table/{serverId}/{schema}/{table}"; } - - public String getTableStatus() - { + + public String getTableStatus() { return agentUrl + "/statement/show-table-status/{serverId}/{schema}/{table}"; } - - public String getSqlAdvisor() - { + + public String getSqlAdvisor() { return agentUrl + "/statement/sql-advisor?serverId={serverId}&schema={schema}&sql={sql}"; } - - public String getSlowStatement() - { + + public String getSlowStatement() { return agentUrl + "/statement/slow/{serverId}/{orderBy}/{pageIndex}/{pageSize}"; } - - public String getDatabases() - { + + public String getDatabases() { return agentUrl + "/mysql/{serverId}/databases"; } - - public String getTables() - { + + public String getTables() { return agentUrl + "/mysql/{serverId}/{schema}/tables"; } - - public String getPriv() - { + + public String getPriv() { return agentUrl + "/mysql/{serverId}/{schema}/priv"; } - - public String userAdd() - { + + public String userAdd() { return agentUrl + "/mysql/add/user/{serverId}/{schema}/{user}/{host}/{password}/{createRepUser}"; } - - public String executeSql() - { + + public String executeSql() { return agentUrl + "/statement/execute?serverId={serverId}&schema={schema}&sql={sql}"; } - - public String getStatus() - { + + public String getStatus() { return agentUrl + "/mysql/get/status/{serverId}"; } - - public String getNewSqlAdvisor() - { + + public String getNewSqlAdvisor() { return agentUrl + "/sql-adviser/advice?serverId={serverId}&schema={schema}&sql={sql}"; } - - public String getIndexCardinality() - { + + public String getIndexCardinality() { return agentUrl + "/index/{serverId}/{pageIndex}/{pageSize}/index-cardinality"; } - - public String getRedundantIndexes() - { + + public String getRedundantIndexes() { return agentUrl + "/index/{serverId}/{pageIndex}/{pageSize}/redundant-indexes"; } - - public String getUnusedIndexes() - { + + public String getUnusedIndexes() { return agentUrl + "/index/{serverId}/{pageIndex}/{pageSize}/unused-indexes"; } - - public String getAllNodesByAgent() - { + + public String getAllNodesByAgent() { return agentUrl + "/replication/status"; } - - public String getReplicationAdd() - { + + public String getReplicationAdd() { return agentUrl + "/replication/addMasterSlave/{masterServerId}/{masterUser}/{masterPassword}/{slaveServerId}"; } - - public String getReplicationAddDouble() - { + + public String getReplicationAddDouble() { return agentUrl - + "/replication/addDoubleMaster/{master1ServerId}/{master1User}/{master1Password}/{master2ServerId}/{master2User}/{master2Password}"; + + "/replication/addDoubleMaster/{master1ServerId}/{master1User}/{master1Password}/{master2ServerId}/{master2User}/{master2Password}"; } - - public String getRelieveReplication() - { + + public String getRelieveReplication() { return agentUrl + "/replication/remove/slave/{serverId}"; } - + }