diff options
| author | Tim <timothyasir@gluster.com> | 2011-06-23 15:40:21 +0530 |
|---|---|---|
| committer | Tim <timothyasir@gluster.com> | 2011-07-01 14:45:00 +0530 |
| commit | 19ed9b4866499d9c264c8db8fc616fb5a5e36ce3 (patch) | |
| tree | 0e213b6464dbb356aa08888c9945022152ee0b8e /src | |
| parent | 9db0e9b38e9721d5e802f910787df58f915d56f7 (diff) | |
| parent | 356b3102e5aa24ab1eaae6d1460401be4d546152 (diff) | |
Updated get_server_details.py to provide raid disk details.
Merge remote branch 'upstream/master'
Diffstat (limited to 'src')
20 files changed, 648 insertions, 329 deletions
diff --git a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterDataModelManager.java b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterDataModelManager.java index 3a33e4e4..3c5aedf5 100644 --- a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterDataModelManager.java +++ b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterDataModelManager.java @@ -101,11 +101,7 @@ public class GlusterDataModelManager { } private void initializeGlusterServers(Cluster cluster) { - GlusterServerListResponse glusterServerListResponse = new GlusterServersClient().getServers(); - if (!glusterServerListResponse.getStatus().isSuccess()) { - throw new GlusterRuntimeException(glusterServerListResponse.getStatus().getMessage()); - } - cluster.setServers(glusterServerListResponse.getServers()); + cluster.setServers(new GlusterServersClient().getServers()); } private void initializeAutoDiscoveredServers(Cluster cluster) { diff --git a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterServersClient.java b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterServersClient.java index 26129736..b525da01 100644 --- a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterServersClient.java +++ b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/GlusterServersClient.java @@ -23,17 +23,11 @@ import static com.gluster.storage.management.core.constants.RESTConstants.RESOUR import java.util.List; -import javax.ws.rs.core.MultivaluedMap; - import com.gluster.storage.management.core.constants.RESTConstants; import com.gluster.storage.management.core.model.GlusterServer; import com.gluster.storage.management.core.model.Server; -import com.gluster.storage.management.core.model.Status; -import com.gluster.storage.management.core.response.GenericResponse; import com.gluster.storage.management.core.response.GlusterServerListResponse; -import com.gluster.storage.management.core.response.GlusterServerResponse; import com.sun.jersey.api.representation.Form; -import com.sun.jersey.core.util.MultivaluedMapImpl; public class GlusterServersClient extends AbstractClient { @@ -54,19 +48,12 @@ public class GlusterServersClient extends AbstractClient { return RESOURCE_PATH_CLUSTERS + "/" + clusterName + "/" + RESOURCE_SERVERS; } - public GlusterServerListResponse getServers() { - return (GlusterServerListResponse) fetchResource(GlusterServerListResponse.class); + public List<GlusterServer> getServers() { + return ((GlusterServerListResponse) fetchResource(GlusterServerListResponse.class)).getServers(); } - @SuppressWarnings("unchecked") public GlusterServer getGlusterServer(String serverName) { - GenericResponse<GlusterServer> response = (GenericResponse<GlusterServer>) fetchSubResource(serverName, - GenericResponse.class); - return response.getData(); - } - - public String getServerXML(String serverName) { - return ((String) fetchSubResource(serverName, String.class)); + return (GlusterServer) fetchSubResource(serverName, GlusterServer.class); } public void addServer(Server discoveredServer) { @@ -83,7 +70,7 @@ public class GlusterServersClient extends AbstractClient { UsersClient usersClient = new UsersClient(); if (usersClient.authenticate("gluster", "gluster").isSuccess()) { GlusterServersClient glusterServersClient = new GlusterServersClient(usersClient.getSecurityToken(), "cluster1"); - List<GlusterServer> glusterServers = glusterServersClient.getServers().getData(); + List<GlusterServer> glusterServers = glusterServersClient.getServers(); for (GlusterServer server : glusterServers) { System.out.println(server.getName()); } diff --git a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/VolumesClient.java b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/VolumesClient.java index 9a1eb261..d237f010 100644 --- a/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/VolumesClient.java +++ b/src/com.gluster.storage.management.client/src/com/gluster/storage/management/client/VolumesClient.java @@ -20,6 +20,15 @@ */ package com.gluster.storage.management.client; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_ACCESS_PROTOCOLS; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_BRICKS; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_REPLICA_COUNT; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_STRIPE_COUNT; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_TRANSPORT_TYPE; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_VOLUME_NAME; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_VOLUME_OPTIONS; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_VOLUME_TYPE; + import java.util.Date; import java.util.List; @@ -29,7 +38,6 @@ import com.gluster.storage.management.core.constants.CoreConstants; import com.gluster.storage.management.core.constants.GlusterConstants; import com.gluster.storage.management.core.constants.RESTConstants; import com.gluster.storage.management.core.model.Brick; -import com.gluster.storage.management.core.model.Status; import com.gluster.storage.management.core.model.Volume; import com.gluster.storage.management.core.model.VolumeLogMessage; import com.gluster.storage.management.core.model.VolumeOptionInfo; @@ -60,8 +68,17 @@ public class VolumesClient extends AbstractClient { return RESTConstants.RESOURCE_PATH_CLUSTERS + "/" + clusterName + "/" + RESTConstants.RESOURCE_VOLUMES; } - public Status createVolume(Volume volume) { - return (Status) postObject(Status.class, volume); + public void createVolume(Volume volume) { + Form form = new Form(); + form.add(FORM_PARAM_VOLUME_NAME, volume.getName()); + form.add(FORM_PARAM_VOLUME_TYPE, volume.getVolumeType().toString()); + form.add(FORM_PARAM_TRANSPORT_TYPE, volume.getTransportType().toString()); + form.add(FORM_PARAM_REPLICA_COUNT, volume.getReplicaCount()); + form.add(FORM_PARAM_STRIPE_COUNT, volume.getStripeCount()); + form.add(FORM_PARAM_BRICKS, StringUtil.collectionToString(volume.getBricks(), ",")); + form.add(FORM_PARAM_ACCESS_PROTOCOLS, StringUtil.collectionToString(volume.getNASProtocols(), ",")); + form.add(FORM_PARAM_VOLUME_OPTIONS, StringUtil.collectionToString(volume.getOptions().getOptions(), ",")); + postRequest(form); } private void performOperation(String volumeName, String operation) { @@ -120,7 +137,7 @@ public class VolumesClient extends AbstractClient { } public void addBricks(String volumeName, List<String> brickList) { - String bricks = StringUtil.ListToString(brickList, ","); + String bricks = StringUtil.collectionToString(brickList, ","); Form form = new Form(); form.add(RESTConstants.FORM_PARAM_BRICKS, bricks); postRequest(volumeName + "/" + RESTConstants.RESOURCE_BRICKS, form); @@ -159,7 +176,7 @@ public class VolumesClient extends AbstractClient { } public void removeBricks(String volumeName, List<Brick> BrickList, boolean deleteOption) { - String bricks = StringUtil.ListToString(GlusterCoreUtil.getQualifiedBrickList(BrickList), ","); + String bricks = StringUtil.collectionToString(GlusterCoreUtil.getQualifiedBrickList(BrickList), ","); MultivaluedMap<String, String> queryParams = prepareRemoveBrickQueryParams(volumeName, bricks, deleteOption); deleteSubResource(volumeName + "/" + RESTConstants.RESOURCE_BRICKS, queryParams); } diff --git a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/Brick.java b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/Brick.java index 83acb9a3..fef8f969 100644 --- a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/Brick.java +++ b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/Brick.java @@ -86,5 +86,9 @@ public class Brick extends Entity { return StringUtil.filterString(getServerName() + getBrickDirectory() + getDiskName(), filterString, caseSensitive); } - + + @Override + public String toString() { + return getQualifiedName(); + } } diff --git a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/VolumeOption.java b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/VolumeOption.java index 2e3871b6..f4ca3e81 100644 --- a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/VolumeOption.java +++ b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/model/VolumeOption.java @@ -51,4 +51,9 @@ public class VolumeOption { public void setValue(String value) { this.value = value; } + + @Override + public String toString() { + return key + "=" + value; + } } diff --git a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/response/GlusterServerListResponse.java b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/response/GlusterServerListResponse.java index 4c291a33..f375905c 100644 --- a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/response/GlusterServerListResponse.java +++ b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/response/GlusterServerListResponse.java @@ -34,41 +34,24 @@ import com.gluster.storage.management.core.model.Status; /** * */ -@XmlRootElement(name = "response") -public class GlusterServerListResponse extends AbstractResponse { +@XmlRootElement(name = "servers") +public class GlusterServerListResponse { private List<GlusterServer> servers = new ArrayList<GlusterServer>(); public GlusterServerListResponse() { } - public GlusterServerListResponse(Status status, List<GlusterServer> servers) { - setStatus(status); + public GlusterServerListResponse(List<GlusterServer> servers) { setServers(servers); } - @XmlElementWrapper(name = "servers") @XmlElement(name = "server", type=GlusterServer.class) public List<GlusterServer> getServers() { return servers; } - /** - * @param servers - * the servers to set - */ public void setServers(List<GlusterServer> servers) { this.servers = servers; } - - /* - * (non-Javadoc) - * - * @see com.gluster.storage.management.core.model.Response#getData() - */ - @Override - @XmlTransient - public List<GlusterServer> getData() { - return getServers(); - } } diff --git a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/GlusterCoreUtil.java b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/GlusterCoreUtil.java index 14bad4fd..c238cad7 100644 --- a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/GlusterCoreUtil.java +++ b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/GlusterCoreUtil.java @@ -21,9 +21,7 @@ package com.gluster.storage.management.core.utils; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import com.gluster.storage.management.core.model.Brick; import com.gluster.storage.management.core.model.Disk; @@ -46,50 +44,4 @@ public class GlusterCoreUtil { } return qualifiedBricks; } - - /** - * Extracts a list from a string by splitting it on given delimiter - * @param input the input string - * @return A {@link List} of extracted tokens - */ - public List<String> extractList(String input, String delim) { - String[] arr = input.split(delim); - List<String> output = new ArrayList<String>(); - for(String str : arr) { - String brick = str.trim(); - if(!brick.isEmpty()) { - output.add(brick); - } - } - return null; - } - - /** - * Extracts a map from a string by splitting it on the given primary and secondary delimiter. e.g. The input string - * <i>k1=v1,k2=v2,k3=v3</i> will yield the following map:<br> - * k1 -> v1<br> - * k2 -> v2<br> - * k3 -> v3<br> - * where <b>,</b> is the primary delimiter and <b>=</b> is the secondary delimiter. - * - * @param input - * @param majorDelim - * @param minorDelim - * @return Map of key value pairs - */ - public Map<String, String> extractMap(String input, String majorDelim, String minorDelim) { - String[] arr = input.split(majorDelim); - Map<String, String> output = new LinkedHashMap<String, String>(); - for(String str : arr) { - String[] elements = str.split(minorDelim); - if(elements.length == 2) { - String key = elements[0].trim(); - String value = elements[1].trim(); - if(!key.isEmpty() && !value.isEmpty()) { - output.put(key, value); - } - } - } - return output; - } } diff --git a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/StringUtil.java b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/StringUtil.java index 1b7335ee..2fce81cb 100644 --- a/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/StringUtil.java +++ b/src/com.gluster.storage.management.core/src/com/gluster/storage/management/core/utils/StringUtil.java @@ -19,7 +19,10 @@ package com.gluster.storage.management.core.utils; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class StringUtil { public static boolean filterString(String sourceString, String filterString, boolean caseSensitive) { @@ -31,13 +34,13 @@ public class StringUtil { return str.replaceAll("\\s+", ""); } - public static String ListToString(List<String> list, String delimiter) { + public static String collectionToString(Collection<? extends Object> list, String delimiter) { if (list.size() == 0) { return ""; } StringBuilder output = new StringBuilder(); - for (String element : list) { - output.append(element).append(delimiter); + for (Object element : list) { + output.append(element.toString()).append(delimiter); } String outputStr = output.toString(); int endIndex = outputStr.length() - delimiter.length(); @@ -51,18 +54,64 @@ public class StringUtil { } return enumAsArray; } + + /** + * Extracts a list from a string by splitting it on given delimiter + * @param input the input string + * @return A {@link List} of extracted tokens + */ + public static List<String> extractList(String input, String delim) { + String[] arr = input.split(delim); + List<String> output = new ArrayList<String>(); + for(String str : arr) { + String brick = str.trim(); + if(!brick.isEmpty()) { + output.add(brick); + } + } + return output; + } + + /** + * Extracts a map from a string by splitting it on the given primary and secondary delimiter. e.g. The input string + * <i>k1=v1,k2=v2,k3=v3</i> will yield the following map:<br> + * k1 -> v1<br> + * k2 -> v2<br> + * k3 -> v3<br> + * where <b>,</b> is the primary delimiter and <b>=</b> is the secondary delimiter. + * + * @param input + * @param majorDelim + * @param minorDelim + * @return Map of key value pairs + */ + public static Map<String, String> extractMap(String input, String majorDelim, String minorDelim) { + String[] arr = input.split(majorDelim); + Map<String, String> output = new LinkedHashMap<String, String>(); + for(String str : arr) { + String[] elements = str.split(minorDelim); + if(elements.length == 2) { + String key = elements[0].trim(); + String value = elements[1].trim(); + if(!key.isEmpty() && !value.isEmpty()) { + output.put(key, value); + } + } + } + return output; + } public static void main(String args[]) { //Test case for "ListToString" List<String> string = new ArrayList<String>(); // Empty list - System.out.println(StringUtil.ListToString(string, ", ")); + System.out.println(StringUtil.collectionToString(string, ", ")); // Only one string.add("test"); - System.out.println(StringUtil.ListToString(string, ",:")); + System.out.println(StringUtil.collectionToString(string, ",:")); // Multiple string.add("welcome to java"); - System.out.println(StringUtil.ListToString(string, "")); + System.out.println(StringUtil.collectionToString(string, "")); } } diff --git a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/actions/RemoveDiskAction.java b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/actions/RemoveDiskAction.java index 0cad5945..9df40457 100644 --- a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/actions/RemoveDiskAction.java +++ b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/actions/RemoveDiskAction.java @@ -40,7 +40,7 @@ public class RemoveDiskAction extends AbstractActionDelegate { List<String> brickList = getBrickList(bricks); Integer deleteOption = new MessageDialog(getShell(), "Remove Bricks(s)", GUIHelper.getInstance() .getImage(IImageKeys.VOLUME), "Are you sure you want to remove following bricks from volume [" - + volume.getName() + "] ? \n" + StringUtil.ListToString(brickList, ", "), + + volume.getName() + "] ? \n" + StringUtil.collectionToString(brickList, ", "), MessageDialog.QUESTION, new String[] { "Cancel", "Remove bricks, delete data", "Remove bricks, keep data" }, -1).open(); if (deleteOption <= 0) { // By Cancel button(0) or Escape key(-1) diff --git a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/AddDiskWizard.java b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/AddDiskWizard.java index f41a113e..bb55ece1 100644 --- a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/AddDiskWizard.java +++ b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/AddDiskWizard.java @@ -74,7 +74,7 @@ public class AddDiskWizard extends Wizard { GlusterDataModelManager.getInstance().addBricks(volume, bricks); MessageDialog.openInformation(getShell(), "Add brick(s) to Volume", "Volume [" + volume.getName() - + "] is expanded with bricks [" + StringUtil.ListToString(brickList, ", ") + "]"); + + "] is expanded with bricks [" + StringUtil.collectionToString(brickList, ", ") + "]"); return true; } catch (Exception e) { MessageDialog.openError(getShell(), "Add brick(s) to Volume", e.getMessage()); diff --git a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/CreateVolumeWizard.java b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/CreateVolumeWizard.java index 4575edff..ff4bdd98 100644 --- a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/CreateVolumeWizard.java +++ b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/dialogs/CreateVolumeWizard.java @@ -23,87 +23,97 @@ import org.eclipse.jface.wizard.Wizard; import com.gluster.storage.management.client.GlusterDataModelManager; import com.gluster.storage.management.client.VolumesClient; -import com.gluster.storage.management.core.model.Status; import com.gluster.storage.management.core.model.Volume; import com.gluster.storage.management.core.model.Volume.VOLUME_STATUS; public class CreateVolumeWizard extends Wizard { - + private static final String title = "Gluster Management Console - Create Volume"; + private CreateVolumePage1 page; + public CreateVolumeWizard() { - setWindowTitle("Gluster Management Console - Create Volume"); + setWindowTitle(title); setHelpAvailable(false); // TODO: Introduce wizard help } @Override public void addPages() { - addPage(new CreateVolumePage1()); + page = new CreateVolumePage1(); + addPage(page); } @Override public boolean performFinish() { - String dialogTitle = "Create Volume"; - CreateVolumePage1 page = (CreateVolumePage1) getPage(CreateVolumePage1.PAGE_NAME); - Volume newVolume = page.getVolume(); VolumesClient volumesClient = new VolumesClient(); - Status status = volumesClient.createVolume(newVolume); - String message = ""; + + try { + volumesClient.createVolume(newVolume); + handleSuccess(newVolume, volumesClient); + } catch(Exception e) { + String errMsg = e.getMessage(); + // the error could be in to post-volume-create processing. check if this is the case. + if (volumesClient.volumeExists(newVolume.getName())) { + handlePartSuccess(newVolume, volumesClient, errMsg); + } else { + MessageDialog.openError(getShell(), title, "Volume creation failed! Error: " + errMsg); + } + } + + return true; + } + + public void handleSuccess(Volume newVolume, VolumesClient volumesClient) { + String message = "Volume created successfully!"; + newVolume.setStatus(VOLUME_STATUS.OFFLINE); boolean warning = false; - if (status.isSuccess()) { - message = "Volume created successfully!"; - newVolume.setStatus(VOLUME_STATUS.OFFLINE); - if (page.startVolumeAfterCreation()) { + if (page.startVolumeAfterCreation()) { + try { + volumesClient.startVolume(newVolume.getName()); + newVolume.setStatus(VOLUME_STATUS.ONLINE); + message = "Volume created and started successfully!"; + } catch(Exception e) { + message = "Volume created successfuly, but couldn't be started. Error: " + e.getMessage(); + warning = true; + } + } + + // update the model + GlusterDataModelManager.getInstance().addVolume(newVolume); + if (warning) { + MessageDialog.openWarning(getShell(), title, message); + } else { + MessageDialog.openInformation(getShell(), title, message); + } + } + + public void handlePartSuccess(Volume newVolume, VolumesClient volumesClient, String errMsg) { + // volume exists. error was in post-volume-create + newVolume.setStatus(VOLUME_STATUS.OFFLINE); + boolean error = false; + String message1 = null; + if (page.startVolumeAfterCreation()) { + if (MessageDialog.openConfirm(getShell(), title, + "Volume created, but following error(s) occured: " + errMsg + + "\n\nDo you still want to start the volume [" + newVolume.getName() + "]?")) { try { volumesClient.startVolume(newVolume.getName()); newVolume.setStatus(VOLUME_STATUS.ONLINE); - message = "Volume created and started successfully!"; - } catch(Exception e) { - message = "Volume created successfuly, but couldn't be started. Error: " + e.getMessage(); - warning = true; + message1 = "Volume [" + newVolume.getName() + "] started successfully!"; // Only start operation + } catch(Exception e1) { + message1 = "Volume couldn't be started. Error: " + e1.getMessage(); + error = true; } } - // update the model - GlusterDataModelManager.getInstance().addVolume(newVolume); - if (warning) { - MessageDialog.openWarning(getShell(), dialogTitle, message); - } else { - MessageDialog.openInformation(getShell(), dialogTitle, message); - } - } else { - if (status.isPartSuccess()) { - newVolume.setStatus(VOLUME_STATUS.OFFLINE); - boolean error = false; - if (page.startVolumeAfterCreation()) { - if (MessageDialog.openConfirm(getShell(), dialogTitle, - "Volume created, but following error(s) occured: " + status - + "\n\nDo you still want to start the volume [" + newVolume.getName() + "]?")) { - try { - volumesClient.startVolume(newVolume.getName()); - newVolume.setStatus(VOLUME_STATUS.ONLINE); - message = "Volume [" + newVolume.getName() + "] started successfully!"; // Only start operation - } catch(Exception e) { - message = "Volume couldn't be started. Error: " + e.getMessage(); - error = true; - } - } - if (error) { - MessageDialog.openWarning(getShell(), dialogTitle, message); - } else { - MessageDialog.openInformation(getShell(), dialogTitle, message); - } - - } else { // Start volume is not checked - MessageDialog.openWarning(getShell(), dialogTitle, "Volume created, but following error(s) occured: " - + status); - } - GlusterDataModelManager.getInstance().addVolume(newVolume); - + if (error) { + MessageDialog.openWarning(getShell(), title, message1); } else { - MessageDialog.openError(getShell(), dialogTitle, "Volume creation failed! " + status); + MessageDialog.openInformation(getShell(), title, message1); } + } else { // Start volume is not checked + MessageDialog.openWarning(getShell(), title, + "Volume created, but following error(s) occured: " + errMsg); } - - return true; + GlusterDataModelManager.getInstance().addVolume(newVolume); } } diff --git a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/VolumeSummaryView.java b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/VolumeSummaryView.java index c708100d..17bb28f4 100644 --- a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/VolumeSummaryView.java +++ b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/VolumeSummaryView.java @@ -215,7 +215,7 @@ public class VolumeSummaryView extends ViewPart { } } } - return StringUtil.ListToString(OnlineServers, ", ") + ((OnlineServers.size() > maxServers) ? "..." : ""); + return StringUtil.collectionToString(OnlineServers, ", ") + ((OnlineServers.size() > maxServers) ? "..." : ""); } /** diff --git a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/pages/VolumeLogsPage.java b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/pages/VolumeLogsPage.java index 9bff0213..6ba9c0d1 100644 --- a/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/pages/VolumeLogsPage.java +++ b/src/com.gluster.storage.management.gui/src/com/gluster/storage/management/gui/views/pages/VolumeLogsPage.java @@ -46,16 +46,12 @@ import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.widgets.FormToolkit; -import com.gluster.storage.management.client.GlusterDataModelManager; import com.gluster.storage.management.client.VolumesClient; import com.gluster.storage.management.core.constants.CoreConstants; import com.gluster.storage.management.core.constants.GlusterConstants; import com.gluster.storage.management.core.constants.GlusterConstants.VOLUME_LOG_LEVELS; -import com.gluster.storage.management.core.model.VolumeLogMessage; -import com.gluster.storage.management.core.model.Status; import com.gluster.storage.management.core.model.Volume; -import com.gluster.storage.management.core.response.LogMessageListResponse; -import com.gluster.storage.management.core.utils.GlusterCoreUtil; +import com.gluster.storage.management.core.model.VolumeLogMessage; import com.gluster.storage.management.gui.VolumeLogTableLabelProvider; import com.gluster.storage.management.gui.utils.GUIHelper; diff --git a/src/com.gluster.storage.management.server.scripts/src/FsTabUtils.py b/src/com.gluster.storage.management.server.scripts/src/FsTabUtils.py new file mode 100644 index 00000000..fcac4196 --- /dev/null +++ b/src/com.gluster.storage.management.server.scripts/src/FsTabUtils.py @@ -0,0 +1,92 @@ +# Copyright (C) 2010 Gluster, Inc. <http://www.gluster.com> +# This file is part of Gluster Storage Platform. +# +# Gluster Storage Platform is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of +# the License, or (at your option) any later version. +# +# Gluster Storage Platform 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# <http://www.gnu.org/licenses/>. + +import Globals + +def readFsTab(fsTabFile=Globals.FSTAB_FILE): + try: + fsTabfp = open(fsTabFile) + except IOError, e: + log("readFsTab(): " + str(e)) + return None + + fsTabEntryList = [] + for line in fsTabfp: + tokens = line.strip().split() + if not tokens or tokens[0].startswith('#'): + continue + fsTabEntry = {} + fsTabEntry["Device"] = None + fsTabEntry["MountPoint"] = None + fsTabEntry["FsType"] = None + fsTabEntry["Options"] = None + fsTabEntry["DumpOption"] = 0 + fsTabEntry["fsckOrder"] = 0 + try: + fsTabEntry["Device"] = tokens[0] + fsTabEntry["MountPoint"] = tokens[1] + fsTabEntry["FsType"] = tokens[2] + fsTabEntry["Options"] = tokens[3] + fsTabEntry["DumpOption"] = tokens[4] + fsTabEntry["fsckOrder"] = tokens[5] + except IndexError: + pass + if fsTabEntry["Device"] and fsTabEntry["MountPoint"] and fsTabEntry["FsType"] and fsTabEntry["Options"]: + fsTabEntryList.append(fsTabEntry) + + fsTabfp.close() + return fsTabEntryList + +def writeFsTab(fsTabEntryList, fsTabFile=Globals.FSTAB_FILE): + try: + fsTabfp = open(fsTabFile, "w") + for fsTabEntry in fsTabEntryList: + fsTabfp.write("%s\t%s\t%s\t%s\t%s\t%s\n" % + (fsTabEntry["Device"], fsTabEntry["MountPoint"], + fsTabEntry["FsType"], fsTabEntry["Options"], + fsTabEntry["DumpOption"], fsTabEntry["fsckOrder"])) + fsTabfp.close() + except IOError, e: + log("writeFsTab(): " + str(e)) + return False + return True + +def addFsTabEntry(fsTabEntry, fsTabFile=Globals.FSTAB_FILE): + try: + fsTabfp = open(fsTabFile, "a") + fsTabfp.write("%s\t%s\t%s\t%s\t%s\t%s\n" % + (fsTabEntry["Device"], fsTabEntry["MountPoint"], + fsTabEntry["FsType"], fsTabEntry["Options"], + fsTabEntry["DumpOption"], fsTabEntry["fsckOrder"])) + fsTabfp.close() + except IOError, e: + log("addFsTabEntry(): " + str(e)) + return False + return True + +def removeFsTabEntry(fsTabEntry, fsTabFile=Globals.FSTAB_FILE): + fsTabEntryList = readFsTab(fsTabFile) + if not fsTabEntryList: + return False + + try: + fsTabEntryList.remove(fsTabEntry) + except ValueError: + return False + + return writeFsTab(fsTabEntryList, fsTabFile) + diff --git a/src/com.gluster.storage.management.server.scripts/src/RRDUtils.py b/src/com.gluster.storage.management.server.scripts/src/RRDUtils.py new file mode 100644 index 00000000..1ad0deee --- /dev/null +++ b/src/com.gluster.storage.management.server.scripts/src/RRDUtils.py @@ -0,0 +1,72 @@ +import rrdtool +import os +from socket import gethostname +from itertools import groupby + +class RRD: + def __init__ (self): + self.COLORS = [0xff7777, 0x7777ff, 0x55ff55, 0xffcc77, 0xff77ff, 0x77ffff,0xffff77, 0x55aaff] + self.HOST = gethostname() + self.DIR = "/var/lib/collectd" + + def fade_component(self, component): + return ((component + 255 * 5) / 6) + + def fade_color(self, color): + r = 0; + for i in [0,1,2]: + shft = (i * 8) + component = ((color >> shft) & 255) + r |= (self.fade_component(component) << shft) + return r + + def generate_pngs(self): + + rrdlist = os.popen ("find %s -type f -name '*.rrd'" % self.DIR) + + for rrd in rrdlist: + self.dss = [] + self.defs = "" + + rrdinfo = rrdtool.info(rrd.strip()) + + for key in rrdinfo.keys(): + if key.split('[')[0] == 'ds': + self.dss.append(key.split('[')[1].split(']')[0]) + self.dss.sort() + + self.dss = [a for a,b in groupby(self.dss)] + + for ds in self.dss: + self.defs = self.defs + " DEF:%s_avg=%s:%s:AVERAGE " % (ds, rrd.strip(), ds) + self.defs = self.defs + " DEF:%s_max=%s:%s:MAX " % (ds, rrd.strip(), ds) + + j = 0 + for ds in self.dss: + color = self.COLORS[j % len(self.COLORS)] + j = j + 1 + faded_color = self.fade_color(color) + self.defs = self.defs + " AREA:%s_max#%06x " % (ds, faded_color) + + j = 0 + for ds in self.dss: + color = self.COLORS[j % len(self.COLORS)] + j = j + 1 + self.defs = self.defs + " LINE2:%s_avg#%06x:%s " % (ds, color, ds) + self.defs = self.defs + " GPRINT:%s_avg:AVERAGE:%%5.1lf%%sAvg " % ds + self.defs = self.defs + " GPRINT:%s_max:MAX:%%5.1lf%%sMax " % ds + + for span in ['1hour', '1day', '1week', '1month']: + os.system ("mkdir -p %s/%s" % (self.DIR, self.HOST)) + image = os.path.dirname(rrd.strip()) + "-" + span + ".png" + cmd = "rrdtool graph " + image + " -t \"%s %s\"" % (os.path.dirname(rrd.strip()), span) + " --imgformat PNG --width 600 --height 100 --start now-" + span + " --end now --interlaced " + self.defs + " >/dev/null 2>&1" + os.system(cmd) + + +def main (): + + rrd = RRD () + rrd.generate_pngs () + +if __name__ == "__main__": + main() diff --git a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractResource.java b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractResource.java index d96ea5f5..ee65add0 100644 --- a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractResource.java +++ b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractResource.java @@ -50,7 +50,7 @@ public class AbstractResource { * @return the {@link Response} object */ protected Response createdResponse(String relativePath) { - return Response.created(createURI(relativePath)).build(); + return Response.created(createRelatriveURI(relativePath)).build(); } /** @@ -62,15 +62,41 @@ public class AbstractResource { } /** + * Creates a response with HTTP status code of 202 (accepted), also setting the location header to given location. + * This is typically done while triggering long running tasks + * + * @param uriElements + * URI Elements to be appended to the base URI + * @return the {@link Response} object + */ + protected Response acceptedResponse(Object...uriElements) { + return Response.status(Status.ACCEPTED).location(createAbsoluteURI(uriElements)).build(); + } + + /** + * Creates a new URI that is relative to the <b>base URI</b> of the application + * @param uriElements URI Elements to be appended to the base URI + * @return newly created URI + */ + private URI createAbsoluteURI(Object[] uriElements) { + return uriInfo.getBaseUriBuilder().build(uriElements); + } + + /** * Creates a response with HTTP status code of 204 (no content), also setting the location header to given location * @param location path of the location to be set relative to current path * @return the {@link Response} object */ protected Response noContentResponse(String location) { - return Response.noContent().location(createURI(location)).build(); + return Response.noContent().location(createRelatriveURI(location)).build(); } - protected URI createURI(String location) { + /** + * Creates a URI relative to current URI + * @param location path relative to current URI + * @return newly created URI + */ + protected URI createRelatriveURI(String location) { return uriInfo.getAbsolutePathBuilder().path(location).build(); } diff --git a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractServersResource.java b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractServersResource.java index ec396539..0bc0f061 100644 --- a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractServersResource.java +++ b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/AbstractServersResource.java @@ -30,7 +30,7 @@ import com.sun.jersey.api.core.InjectParam; /** * Abstract resource class for servers. Abstracts basic server related functionality like "get server details". */ -public class AbstractServersResource { +public class AbstractServersResource extends AbstractResource { @InjectParam protected ServerUtil serverUtil; diff --git a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/GlusterServersResource.java b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/GlusterServersResource.java index 48c9b7fc..6e15e106 100644 --- a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/GlusterServersResource.java +++ b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/GlusterServersResource.java @@ -18,7 +18,9 @@ *******************************************************************************/ package com.gluster.storage.management.server.resources; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_CLUSTER_NAME; import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_SERVER_NAME; +import static com.gluster.storage.management.core.constants.RESTConstants.FORM_PARAM_SOURCE; import static com.gluster.storage.management.core.constants.RESTConstants.PATH_PARAM_CLUSTER_NAME; import static com.gluster.storage.management.core.constants.RESTConstants.PATH_PARAM_SERVER_NAME; import static com.gluster.storage.management.core.constants.RESTConstants.RESOURCE_PATH_CLUSTERS; @@ -35,12 +37,14 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.gluster.storage.management.core.constants.CoreConstants; import com.gluster.storage.management.core.exceptions.ConnectionException; +import com.gluster.storage.management.core.exceptions.GlusterRuntimeException; import com.gluster.storage.management.core.model.GlusterServer; import com.gluster.storage.management.core.model.GlusterServer.SERVER_STATUS; import com.gluster.storage.management.core.model.Status; @@ -121,143 +125,210 @@ public class GlusterServersResource extends AbstractServersResource { } @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getGlusterServersJSON( + @PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName) { + return getGlusterServers(clusterName, MediaType.APPLICATION_JSON); + } + + @GET @Produces(MediaType.APPLICATION_XML) - public GlusterServerListResponse getGlusterServers( + public Response getGlusterServersXML( @PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName) { + return getGlusterServers(clusterName, MediaType.APPLICATION_XML); + } + + public Response getGlusterServers(String clusterName, String mediaType) { List<GlusterServer> glusterServers = new ArrayList<GlusterServer>(); + if (clusterName == null || clusterName.isEmpty()) { + return badRequestResponse("Cluster name must not be empty!"); + } + ClusterInfo cluster = clusterService.getCluster(clusterName); - if(cluster == null) { - return new GlusterServerListResponse(new Status(Status.STATUS_CODE_FAILURE, "Cluster [" + clusterName - + "] doesn't exist!"), null); + if (cluster == null) { + return badRequestResponse("Cluster [" + clusterName + "] not found!"); } - + if(cluster.getServers().size() == 0) { - return new GlusterServerListResponse(Status.STATUS_SUCCESS, glusterServers); + return okResponse(new GlusterServerListResponse(glusterServers), mediaType); } GlusterServer onlineServer = getOnlineServer(clusterName); - if(onlineServer == null) { - return new GlusterServerListResponse(new Status(Status.STATUS_CODE_FAILURE, - "No online server found in cluster [" + clusterName + "]"), glusterServers); + if (onlineServer == null) { + return errorResponse("No online servers found in cluster [" + clusterName + "]"); } try { - glusterServers = glusterUtil.getGlusterServers(onlineServer); + glusterServers = getGlusterServers(clusterName, onlineServer); } catch(ConnectionException e) { // online server has gone offline! try with a different one. onlineServer = getNewOnlineServer(clusterName); - if(onlineServer == null) { - return new GlusterServerListResponse(new Status(Status.STATUS_CODE_FAILURE, - "No online server found in cluster [" + clusterName + "]"), glusterServers); + if (onlineServer == null) { + return errorResponse("No online servers found in cluster [" + clusterName + "]"); } - - glusterServers = glusterUtil.getGlusterServers(onlineServer); + try { + glusterServers = getGlusterServers(clusterName, onlineServer); + } catch(Exception e1) { + return errorResponse(e1.getMessage()); + } + } catch(Exception e) { + return errorResponse(e.getMessage()); } - int errCount = 0; - StringBuilder errMsg = new StringBuilder("Couldn't fetch details for server(s): "); + String errMsg = fetchDetailsOfServers(glusterServers, onlineServer); + if(!errMsg.isEmpty()) { + return errorResponse("Couldn't fetch details for server(s): " + errMsg); + } + + return okResponse(new GlusterServerListResponse(glusterServers), mediaType); + } + + public String fetchDetailsOfServers(List<GlusterServer> glusterServers, GlusterServer onlineServer) { + String errMsg = ""; for (GlusterServer server : glusterServers) { if (server.getStatus() == SERVER_STATUS.ONLINE && !server.getName().equals(onlineServer.getName())) { try { fetchServerDetails(server); } catch (Exception e) { - errMsg.append(CoreConstants.NEWLINE + server.getName() + " : [" + e.getMessage() + "]"); - errCount++; + errMsg += CoreConstants.NEWLINE + server.getName() + " : [" + e.getMessage() + "]"; } } } - Status status; - if (errCount==0) { - status = new Status(Status.STATUS_CODE_SUCCESS, "Success"); - } else if(errCount == glusterServers.size()) { - status = new Status(Status.STATUS_CODE_FAILURE, errMsg.toString()); - } else { - status = new Status(Status.STATUS_CODE_PART_SUCCESS, errMsg.toString()); + return errMsg; + } + + public List<GlusterServer> getGlusterServers(String clusterName, GlusterServer onlineServer) { + List<GlusterServer> glusterServers; + try { + glusterServers = glusterUtil.getGlusterServers(onlineServer); + } catch(ConnectionException e) { + // online server has gone offline! try with a different one. + onlineServer = getNewOnlineServer(clusterName); + if(onlineServer == null) { + throw new GlusterRuntimeException("No online servers found in cluster [" + clusterName + "]"); + } + + glusterServers = glusterUtil.getGlusterServers(onlineServer); } - return new GlusterServerListResponse(status, glusterServers); + return glusterServers; } @GET @Path("{serverName}") @Produces(MediaType.APPLICATION_XML) - public GlusterServerResponse getGlusterServer( + public Response getGlusterServerXML( @PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @PathParam(PATH_PARAM_SERVER_NAME) String serverName) { - GlusterServer server = glusterUtil.getGlusterServer(getOnlineServer(clusterName), serverName); - Status status = Status.STATUS_SUCCESS; - if(server.isOnline()) { - try { - fetchServerDetails(server); - } catch (Exception e) { - status.setCode(Status.STATUS_CODE_FAILURE); + return getGlusterServerResponse(clusterName, serverName, MediaType.APPLICATION_XML); + } + + @GET + @Path("{serverName}") + @Produces(MediaType.APPLICATION_JSON) + public Response getGlusterServerJSON( + @PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, + @PathParam(PATH_PARAM_SERVER_NAME) String serverName) { + return getGlusterServerResponse(clusterName, serverName, MediaType.APPLICATION_JSON); + } + + private Response getGlusterServerResponse(String clusterName, String serverName, String mediaType) { + try { + return okResponse(getGlusterServer(clusterName, serverName), mediaType); + } catch(Exception e) { + return errorResponse(e.getMessage()); + } + } + + private GlusterServer getGlusterServer(String clusterName, String serverName) { + if (clusterName == null || clusterName.isEmpty()) { + throw new GlusterRuntimeException("Cluster name must not be empty!"); + } + + if (serverName == null || serverName.isEmpty()) { + throw new GlusterRuntimeException("Server name must not be empty!"); + } + + ClusterInfo cluster = clusterService.getCluster(clusterName); + if (cluster == null) { + throw new GlusterRuntimeException("Cluster [" + clusterName + "] not found!"); + } + + GlusterServer onlineServer = getOnlineServer(clusterName); + if (onlineServer == null) { + throw new GlusterRuntimeException("No online servers found in cluster [" + clusterName + "]"); + } + + GlusterServer server = null; + try { + server = glusterUtil.getGlusterServer(onlineServer, serverName); + } catch(ConnectionException e) { + // online server has gone offline! try with a different one. + onlineServer = getNewOnlineServer(clusterName); + if (onlineServer == null) { + throw new GlusterRuntimeException("No online servers found in cluster [" + clusterName + "]"); } + server = glusterUtil.getGlusterServer(onlineServer, serverName); } - return new GlusterServerResponse(status, server); + + if(server.isOnline()) { + fetchServerDetails(server); + } + return server; } - private Status performAddServer(String clusterName, String serverName) { + private void performAddServer(String clusterName, String serverName) { GlusterServer onlineServer = getOnlineServer(clusterName); if(onlineServer == null) { - return new Status(Status.STATUS_CODE_FAILURE, - "No online server found in cluster [" + clusterName + "]"); + throw new GlusterRuntimeException("No online server found in cluster [" + clusterName + "]"); } - Status status; try { - status = glusterUtil.addServer(onlineServer.getName(), serverName); - if(status.isSuccess()) { - // other peer probe to ensure that host names appear in peer probe on both sides - status = glusterUtil.addServer(serverName, onlineServer.getName()); - } + glusterUtil.addServer(onlineServer.getName(), serverName); } catch(ConnectionException e) { + // online server has gone offline! try with a different one. onlineServer = getNewOnlineServer(clusterName); if(onlineServer == null) { - return new Status(Status.STATUS_CODE_FAILURE, - "No online server found in cluster [" + clusterName + "]"); + throw new GlusterRuntimeException("No online server found in cluster [" + clusterName + "]"); } - status = glusterUtil.addServer(serverName, onlineServer.getName()); + + glusterUtil.addServer(serverName, onlineServer.getName()); } - - return status; } @POST - @Produces(MediaType.APPLICATION_XML) - public GlusterServerResponse addServer(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, + public Response addServer(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @FormParam(FORM_PARAM_SERVER_NAME) String serverName) { - if(clusterName.isEmpty()) { - return new GlusterServerResponse( - new Status(Status.STATUS_CODE_FAILURE, "Cluster name should not be empty!"), null); + if(clusterName == null || clusterName.isEmpty()) { + return badRequestResponse("Cluster name must not be empty!"); } if(serverName == null || serverName.isEmpty()) { - return new GlusterServerResponse(new Status(Status.STATUS_CODE_FAILURE, "Form parameter [" - + FORM_PARAM_SERVER_NAME + "] is mandatory!"), null); + return badRequestResponse("Parameter [" + FORM_PARAM_SERVER_NAME + "] is missing in request!"); } ClusterInfo cluster = clusterService.getCluster(clusterName); - if(cluster == null) { - return new GlusterServerResponse(new Status(Status.STATUS_CODE_FAILURE, "Cluster [" + clusterName - + "] doesn't exist!"), null); + if (cluster == null) { + return badRequestResponse("Cluster [" + clusterName + "] not found!"); } boolean publicKeyInstalled = sshUtil.isPublicKeyInstalled(serverName); if(!publicKeyInstalled && !sshUtil.hasDefaultPassword(serverName)) { // public key not installed, default password doesn't work. return with error. - return new GlusterServerResponse(new Status(Status.STATUS_CODE_FAILURE, - "Gluster Management Gateway uses the default password to set up keys on the server." - + CoreConstants.NEWLINE + "However it seems that the password on server [" + serverName - + "] has been changed manually." + CoreConstants.NEWLINE - + "Please reset it back to the standard default password and try again."), null); + return errorResponse("Gluster Management Gateway uses the default password to set up keys on the server." + + CoreConstants.NEWLINE + "However it seems that the password on server [" + serverName + + "] has been changed manually." + CoreConstants.NEWLINE + + "Please reset it back to the standard default password and try again."); } List<ServerInfo> servers = cluster.getServers(); if(servers != null && !servers.isEmpty()) { - Status status = performAddServer(clusterName, serverName); - if(!status.isSuccess()) { - return new GlusterServerResponse(status, null); + // cluster has at least one existing server, so that peer probe can be performed + try { + performAddServer(clusterName, serverName); + } catch(Exception e) { + return errorResponse(e.getMessage()); } } else { // this is the first server to be added to the cluster, which means no @@ -268,100 +339,101 @@ public class GlusterServersResource extends AbstractServersResource { // add the cluster-server mapping clusterService.mapServerToCluster(clusterName, serverName); } catch (Exception e) { - return new GlusterServerResponse(new Status(Status.STATUS_CODE_PART_SUCCESS, e.getMessage()), null); + return errorResponse(e.getMessage()); } // since the server is added to a cluster, it should not more be considered as a // discovered server available to other clusters discoveredServersResource.removeDiscoveredServer(serverName); - // fetch server details - GlusterServerResponse serverResponse = getGlusterServer(clusterName, serverName); - if (!publicKeyInstalled) { try { // install public key (this will also disable password based ssh login) sshUtil.installPublicKey(serverName); } catch (Exception e) { - return new GlusterServerResponse(new Status(Status.STATUS_CODE_PART_SUCCESS, - "Public key could not be installed! Error: [" + e.getMessage() + "]"), - serverResponse.getGlusterServer()); + return errorResponse("Public key could not be installed on [" + serverName + "]! Error: [" + + e.getMessage() + "]"); } } - return serverResponse; + return createdResponse(serverName); } @DELETE - @Produces(MediaType.APPLICATION_XML) @Path("{" + PATH_PARAM_SERVER_NAME + "}") - public Status removeServer(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, + public Response removeServer(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @PathParam(PATH_PARAM_SERVER_NAME) String serverName) { - if (clusterName.isEmpty()) { - return new Status(Status.STATUS_CODE_FAILURE, "Cluster name should not be empty!"); + if (clusterName == null || clusterName.isEmpty()) { + return badRequestResponse("Cluster name must not be empty!"); } if(serverName == null || serverName.isEmpty()) { - return new Status(Status.STATUS_CODE_FAILURE, "Form parameter [" + FORM_PARAM_SERVER_NAME - + "] is mandatory!"); + return badRequestResponse("Server name must not be empty!"); } ClusterInfo cluster = clusterService.getCluster(clusterName); if(cluster == null) { - return new Status(Status.STATUS_CODE_FAILURE, "Cluster [" + clusterName + "] doesn't exist!"); + return badRequestResponse("Cluster [" + clusterName + "] not found!"); } List<ServerInfo> servers = cluster.getServers(); - if(servers == null || servers.isEmpty()) { - return new Status(Status.STATUS_CODE_FAILURE, "Server [" + serverName + "] is not attached to cluster [" + if(servers == null || servers.isEmpty() || !containsServer(servers, serverName)) { + return badRequestResponse("Server [" + serverName + "] is not attached to cluster [" + clusterName + "]!"); } - Status status = Status.STATUS_SUCCESS; if(servers.size() == 1) { // Only one server mapped to the cluster, no "peer detach" required. // remove the cached online server for this cluster if present clusterServerCache.remove(clusterName); } else { - // get an online server that is not same as the server being removed - GlusterServer onlineServer = getOnlineServer(clusterName, serverName); - if (onlineServer == null) { - return new Status(Status.STATUS_CODE_FAILURE, "No online server found in cluster [" + clusterName + "]"); - } - try { - status = glusterUtil.removeServer(onlineServer.getName(), serverName); - } catch (ConnectionException e) { - // online server has gone offline! try with a different one. - onlineServer = getNewOnlineServer(clusterName, serverName); - if (onlineServer == null) { - return new Status(Status.STATUS_CODE_FAILURE, "No online server found in cluster [" + clusterName - + "]"); - } - status = glusterUtil.removeServer(onlineServer.getName(), serverName); - if(!status.isSuccess()) { - return status; - } + removeServerFromCluster(clusterName, serverName); + } catch(Exception e) { + return errorResponse(e.getMessage()); } - - if(onlineServer.getName().equals(serverName)) { - // since the cached server has been removed from the cluster, remove it from the cache - clusterServerCache.remove(clusterName); + } + + return noContentResponse(); + } + + private void removeServerFromCluster(String clusterName, String serverName) { + // get an online server that is not same as the server being removed + GlusterServer onlineServer = getOnlineServer(clusterName, serverName); + if (onlineServer == null) { + throw new GlusterRuntimeException("No online server found in cluster [" + clusterName + "]"); + } + + try { + glusterUtil.removeServer(onlineServer.getName(), serverName); + } catch (ConnectionException e) { + // online server has gone offline! try with a different one. + onlineServer = getNewOnlineServer(clusterName, serverName); + if (onlineServer == null) { + throw new GlusterRuntimeException("No online server found in cluster [" + clusterName + "]"); } + glusterUtil.removeServer(onlineServer.getName(), serverName); } - - try { - clusterService.unmapServerFromCluster(clusterName, serverName); - } catch (Exception e) { - return new Status(Status.STATUS_CODE_PART_SUCCESS, e.getMessage()); + if(onlineServer.getName().equals(serverName)) { + // since the cached server has been removed from the cluster, remove it from the cache + clusterServerCache.remove(clusterName); } + clusterService.unmapServerFromCluster(clusterName, serverName); + // since the server is removed from the cluster, it is now available to be added to other clusters. // Hence add it back to the discovered servers list. - discoveredServersResource.addDiscoveredServer(serverName); - - return status; + discoveredServersResource.addDiscoveredServer(serverName); + } + + private boolean containsServer(List<ServerInfo> servers, String serverName) { + for(ServerInfo server : servers) { + if(server.getName().toUpperCase().equals(serverName.toUpperCase())) { + return true; + } + } + return false; } private void setGlusterUtil(GlusterUtil glusterUtil) { diff --git a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/VolumesResource.java b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/VolumesResource.java index 369ef349..5d1f38e9 100644 --- a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/VolumesResource.java +++ b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/resources/VolumesResource.java @@ -142,7 +142,7 @@ public class VolumesResource extends AbstractResource { public Response getVolumes(String clusterName, String mediaType) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -178,7 +178,11 @@ public class VolumesResource extends AbstractResource { @FormParam(FORM_PARAM_REPLICA_COUNT) Integer replicaCount, @FormParam(FORM_PARAM_STRIPE_COUNT) Integer stripeCount, @FormParam(FORM_PARAM_BRICKS) String bricks, @FormParam(FORM_PARAM_ACCESS_PROTOCOLS) String accessProtocols, @FormParam(FORM_PARAM_VOLUME_OPTIONS) String options) { - String missingParam = checkMissingParamsForCreateVolume(clusterName, volumeName, volumeType, transportType, replicaCount, stripeCount, bricks, accessProtocols, options); + if(clusterName == null || clusterName.isEmpty()) { + return badRequestResponse("Cluster name must not be empty!"); + } + + String missingParam = checkMissingParamsForCreateVolume(volumeName, volumeType, transportType, replicaCount, stripeCount, bricks, accessProtocols, options); if(missingParam != null) { return badRequestResponse("Parameter [" + missingParam + "] is missing in request!"); } @@ -221,7 +225,6 @@ public class VolumesResource extends AbstractResource { /** * Returns name of the missing parameter if any. If all parameters are present, - * @param clusterName * @param volumeName * @param volumeType * @param transportType @@ -232,12 +235,11 @@ public class VolumesResource extends AbstractResource { * @param options * @return */ - private String checkMissingParamsForCreateVolume(String clusterName, String volumeName, String volumeType, + private String checkMissingParamsForCreateVolume(String volumeName, String volumeType, String transportType, Integer replicaCount, Integer stripeCount, String bricks, String accessProtocols, String options) { - return (clusterName == null || clusterName.isEmpty()) ? FORM_PARAM_CLUSTER_NAME : - (volumeName == null || volumeName.isEmpty()) ? FORM_PARAM_VOLUME_NAME : + return (volumeName == null || volumeName.isEmpty()) ? FORM_PARAM_VOLUME_NAME : (volumeType == null || volumeType.isEmpty()) ? FORM_PARAM_VOLUME_TYPE : (transportType == null || transportType.isEmpty()) ? FORM_PARAM_TRANSPORT_TYPE : (replicaCount == null) ? FORM_PARAM_REPLICA_COUNT : @@ -268,7 +270,7 @@ public class VolumesResource extends AbstractResource { Volume volume = null; if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -309,7 +311,11 @@ public class VolumesResource extends AbstractResource { public Response performOperation(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @PathParam(PATH_PARAM_VOLUME_NAME) String volumeName, @FormParam(FORM_PARAM_OPERATION) String operation) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); + } + + if (volumeName == null || volumeName.isEmpty()) { + return badRequestResponse("Volume name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -354,15 +360,23 @@ public class VolumesResource extends AbstractResource { @Path("{" + PATH_PARAM_VOLUME_NAME + "}") public Response deleteVolume(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @PathParam(PATH_PARAM_VOLUME_NAME) String volumeName, - @QueryParam(QUERY_PARAM_DELETE_OPTION) boolean deleteFlag) { + @QueryParam(QUERY_PARAM_DELETE_OPTION) Boolean deleteFlag) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty"); } + if (volumeName == null || volumeName.isEmpty()) { + return badRequestResponse("Volume name must not be empty"); + } + if (clusterService.getCluster(clusterName) == null) { return badRequestResponse("Cluster [" + clusterName + "] not found!"); } + if (deleteFlag == null) { + deleteFlag = false; + } + Volume volume = null; try { volume = getVolume(clusterName, volumeName); @@ -390,19 +404,31 @@ public class VolumesResource extends AbstractResource { } @DELETE - @Path("{" + QUERY_PARAM_VOLUME_NAME + "}/" + RESOURCE_BRICKS) + @Path("{" + PATH_PARAM_VOLUME_NAME + "}/" + RESOURCE_BRICKS) public Response removeBricks(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, - @PathParam(QUERY_PARAM_VOLUME_NAME) String volumeName, @QueryParam(QUERY_PARAM_BRICKS) String bricks, - @QueryParam(QUERY_PARAM_DELETE_OPTION) boolean deleteFlag) { + @PathParam(PATH_PARAM_VOLUME_NAME) String volumeName, @QueryParam(QUERY_PARAM_BRICKS) String bricks, + @QueryParam(QUERY_PARAM_DELETE_OPTION) Boolean deleteFlag) { List<String> brickList = Arrays.asList(bricks.split(",")); // Convert from comma separated string (query // parameter) if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); + } + + if (volumeName == null || volumeName.isEmpty()) { + return badRequestResponse("Volume name must not be empty!"); + } + + if (bricks == null || bricks.isEmpty()) { + return badRequestResponse("Parameter [" + QUERY_PARAM_BRICKS + "] is missing in request!"); } if (clusterService.getCluster(clusterName) == null) { return badRequestResponse("Cluster [" + clusterName + "] not found!"); } + + if(deleteFlag == null) { + deleteFlag = false; + } GlusterServer onlineServer = glusterServersResource.getOnlineServer(clusterName); if (onlineServer == null) { @@ -488,11 +514,11 @@ public class VolumesResource extends AbstractResource { @FormParam(RESTConstants.FORM_PARAM_OPTION_KEY) String key, @FormParam(RESTConstants.FORM_PARAM_OPTION_VALUE) String value) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if(volumeName == null || volumeName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_VOLUME_NAME + "] is missing in request!"); + return badRequestResponse("Volume name must not be empty!"); } if(key == null || key.isEmpty()) { @@ -530,7 +556,7 @@ public class VolumesResource extends AbstractResource { return errorResponse(e.getMessage()); } - return noContentResponse(); + return createdResponse(key); } @PUT @@ -538,11 +564,11 @@ public class VolumesResource extends AbstractResource { public Response resetOptions(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, @PathParam(PATH_PARAM_VOLUME_NAME) String volumeName) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if(volumeName == null || volumeName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_VOLUME_NAME + "] is missing in request!"); + return badRequestResponse("Volume name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -631,7 +657,11 @@ public class VolumesResource extends AbstractResource { public Response downloadLogs(@PathParam(PATH_PARAM_CLUSTER_NAME) final String clusterName, @PathParam(PATH_PARAM_VOLUME_NAME) final String volumeName) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); + } + + if (volumeName == null || volumeName.isEmpty()) { + return badRequestResponse("Volume name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -719,11 +749,11 @@ public class VolumesResource extends AbstractResource { public Response getLogs(String clusterName, String volumeName, String brickName, String severity, String fromTimestamp, String toTimestamp, Integer lineCount, String mediaType) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (volumeName == null || volumeName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_VOLUME_NAME + "] is missing in request!"); + return badRequestResponse("Volume name must not be empty!"); } if (clusterService.getCluster(clusterName) == null) { @@ -816,15 +846,15 @@ public class VolumesResource extends AbstractResource { } @POST - @Path("{" + QUERY_PARAM_VOLUME_NAME + "}/" + RESOURCE_BRICKS) + @Path("{" + PATH_PARAM_VOLUME_NAME + "}/" + RESOURCE_BRICKS) public Response addBricks(@PathParam(PATH_PARAM_CLUSTER_NAME) String clusterName, - @PathParam(QUERY_PARAM_VOLUME_NAME) String volumeName, @FormParam(FORM_PARAM_BRICKS) String bricks) { + @PathParam(PATH_PARAM_VOLUME_NAME) String volumeName, @FormParam(FORM_PARAM_BRICKS) String bricks) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (volumeName == null || volumeName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_VOLUME_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (bricks == null || bricks.isEmpty()) { @@ -840,8 +870,9 @@ public class VolumesResource extends AbstractResource { return errorResponse("No online servers found in cluster [" + clusterName + "]"); } + List<String> brickList = Arrays.asList(bricks.split(",")); try { - glusterUtil.addBricks(volumeName, Arrays.asList(bricks.split(",")), onlineServer.getName()); + glusterUtil.addBricks(volumeName, brickList, onlineServer.getName()); } catch (ConnectionException e) { // online server has gone offline! try with a different one. onlineServer = glusterServersResource.getNewOnlineServer(clusterName); @@ -850,7 +881,7 @@ public class VolumesResource extends AbstractResource { } try { - glusterUtil.addBricks(volumeName, Arrays.asList(bricks.split(",")), onlineServer.getName()); + glusterUtil.addBricks(volumeName, brickList, onlineServer.getName()); } catch(Exception e1) { return errorResponse(e1.getMessage()); } @@ -858,7 +889,7 @@ public class VolumesResource extends AbstractResource { return errorResponse(e1.getMessage()); } - return noContentResponse(); + return createdResponse(""); } @PUT @@ -868,18 +899,34 @@ public class VolumesResource extends AbstractResource { @FormParam(FORM_PARAM_TARGET) String toBrick, @FormParam(FORM_PARAM_AUTO_COMMIT) Boolean autoCommit) { if (clusterName == null || clusterName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_CLUSTER_NAME + "] is missing in request!"); + return badRequestResponse("Cluster name must not be empty!"); } if (volumeName == null || volumeName.isEmpty()) { - return badRequestResponse("Parameter [" + FORM_PARAM_VOLUME_NAME + "] is missing in request!"); + return badRequestResponse("Volume name must not be empty!"); + } + + if (fromBrick == null || fromBrick.isEmpty()) { + return badRequestResponse("Parameter [" + FORM_PARAM_SOURCE + "] is missing in request!"); } + if (toBrick == null || toBrick.isEmpty()) { + return badRequestResponse("Parameter [" + FORM_PARAM_TARGET + "] is missing in request!"); + } + + if (clusterService.getCluster(clusterName) == null) { + return badRequestResponse("Cluster [" + clusterName + "] not found!"); + } + GlusterServer onlineServer = glusterServersResource.getOnlineServer(clusterName); if (onlineServer == null) { return errorResponse("No online servers found in cluster [" + clusterName + "]"); } + if(autoCommit == null) { + autoCommit = false; + } + String taskId = null; try { taskId = glusterUtil.migrateBrickStart(volumeName, fromBrick, toBrick, autoCommit, onlineServer.getName()); @@ -896,8 +943,7 @@ public class VolumesResource extends AbstractResource { return errorResponse(e1.getMessage()); } - return noContentResponse(uriInfo.getBaseUri() + "/" + RESTConstants.RESOURCE_PATH_CLUSTERS + "/" + clusterName - + "/" + RESOURCE_TASKS + "/" + taskId); + return acceptedResponse(RESTConstants.RESOURCE_PATH_CLUSTERS, clusterName, RESOURCE_TASKS, taskId); } public static void main(String[] args) throws ClassNotFoundException { diff --git a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/utils/GlusterUtil.java b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/utils/GlusterUtil.java index 3472745e..670ffb5c 100644 --- a/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/utils/GlusterUtil.java +++ b/src/com.gluster.storage.management.server/src/com/gluster/storage/management/server/utils/GlusterUtil.java @@ -30,23 +30,21 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.gluster.storage.management.core.constants.CoreConstants; -import com.gluster.storage.management.core.constants.RESTConstants; import com.gluster.storage.management.core.exceptions.GlusterRuntimeException; import com.gluster.storage.management.core.model.Brick; import com.gluster.storage.management.core.model.Brick.BRICK_STATUS; import com.gluster.storage.management.core.model.GlusterServer; import com.gluster.storage.management.core.model.GlusterServer.SERVER_STATUS; import com.gluster.storage.management.core.model.Status; +import com.gluster.storage.management.core.model.Task.TASK_TYPE; import com.gluster.storage.management.core.model.TaskInfo; import com.gluster.storage.management.core.model.Volume; -import com.gluster.storage.management.core.model.Task.TASK_TYPE; import com.gluster.storage.management.core.model.Volume.TRANSPORT_TYPE; import com.gluster.storage.management.core.model.Volume.VOLUME_STATUS; import com.gluster.storage.management.core.model.Volume.VOLUME_TYPE; -import com.gluster.storage.management.core.model.VolumeOptions; -import com.gluster.storage.management.core.response.TaskResponse; import com.gluster.storage.management.core.utils.GlusterCoreUtil; import com.gluster.storage.management.core.utils.ProcessResult; +import com.gluster.storage.management.core.utils.StringUtil; import com.gluster.storage.management.server.resources.TasksResource; import com.gluster.storage.management.server.tasks.MigrateDiskTask; import com.sun.jersey.api.core.InjectParam; @@ -196,8 +194,19 @@ public class GlusterUtil { return output; } - public Status addServer(String existingServer, String newServer) { - return new Status(sshUtil.executeRemote(existingServer, "gluster peer probe " + newServer)); + public void addServer(String existingServer, String newServer) { + ProcessResult result = sshUtil.executeRemote(existingServer, "gluster peer probe " + newServer); + if(!result.isSuccess()) { + throw new GlusterRuntimeException("Couldn't probe server [" + newServer + "] from [" + existingServer + + "]. Error: " + result); + } + + // reverse peer probe to ensure that host names appear in peer status on both sides + result = sshUtil.executeRemote(newServer, "gluster peer probe " + existingServer); + if(!result.isSuccess()) { + throw new GlusterRuntimeException("Couldn't _reverse_ probe server [" + existingServer + "] from [" + + newServer + "]. Error: " + result); + } } public Status startVolume(String volumeName, String knownServer) { @@ -235,7 +244,7 @@ public class GlusterUtil { TRANSPORT_TYPE transportType = Volume.getTransportTypeByStr(transportTypeStr); transportTypeArg = (transportType == TRANSPORT_TYPE.ETHERNET) ? "tcp" : "rdma"; - String command = prepareVolumeCreateCommand(volumeName, glusterCoreUtil.extractList(bricks, ","), count, + String command = prepareVolumeCreateCommand(volumeName, StringUtil.extractList(bricks, ","), count, volTypeArg, transportTypeArg); ProcessResult result = sshUtil.executeRemote(knownServer, command); if (!result.isSuccess()) { @@ -243,7 +252,7 @@ public class GlusterUtil { } try { - createOptions(volumeName, glusterCoreUtil.extractMap(options, ",", "="), knownServer); + createOptions(volumeName, StringUtil.extractMap(options, ",", "="), knownServer); } catch(Exception e) { throw new GlusterRuntimeException( "Volume created successfully, however following errors occurred while setting options: " @@ -550,8 +559,11 @@ public class GlusterUtil { return new Status(sshUtil.executeRemote(knownServer, command.toString())); } - public Status removeServer(String existingServer, String serverName) { - return new Status(sshUtil.executeRemote(existingServer, "gluster --mode=script peer detach " + serverName)); + public void removeServer(String existingServer, String serverName) { + ProcessResult result = sshUtil.executeRemote(existingServer, "gluster --mode=script peer detach " + serverName); + if(!result.isSuccess()) { + throw new GlusterRuntimeException("Couldn't remove server [" + serverName + "]! Error: " + result); + } } public static void main(String args[]) { |
