diff --git a/hqu/hqapi1/app/ApplicationController.groovy b/hqu/hqapi1/app/ApplicationController.groovy index f48f28c2..bc2478ae 100644 --- a/hqu/hqapi1/app/ApplicationController.groovy +++ b/hqu/hqapi1/app/ApplicationController.groovy @@ -14,9 +14,11 @@ import org.hyperic.hq.appdef.shared.AppdefDuplicateNameException; class ApplicationController extends ApiController { def appMan = AppMan.one - def aBoss = ABoss.one + def aBoss = ABoss.one def resMan = ResMan.one + def failureXml = null + private Closure getApplicationXML(a) { { doc -> Application(id : a.id, @@ -40,198 +42,224 @@ class ApplicationController extends ApiController { } def list(params) { - def failureXml = null - renderXml() { out << ApplicationsResponse() { - if (failureXml) { - out << failureXml - } else { - out << getSuccessXML() - for (app in appMan.getAllApplications(user, PageControl.PAGE_ALL)) { - out << getApplicationXML(app) - } + out << getSuccessXML() + for (app in appMan.getAllApplications(user, PageControl.PAGE_ALL)) { + out << getApplicationXML(app) } } } } - def create(params) { - def createRequest = new XmlParser().parseText(getUpload('postdata')) - def xmlApplication = createRequest['Application'] - - if (!xmlApplication || xmlApplication.size() != 1) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS) - } - } - return - } - - // Validate Resources + /** + * Validate XML within an Application to ensure all passed + * resources are service types. + * @return true if Resources are valid, false otherwise. + */ + private validateApplicationServices(xmlApplication) { for (xmlResource in xmlApplication['Resource']) { def rid = xmlResource.'@id'?.toInteger() def resource = resourceHelper.findById(rid) if (!resource.isService()) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS, - "Invalid resource passed to create, " + - resource.name + " is not a service") - } - } - return + failureXml = getFailureXML(ErrorCode.INVALID_PARAMETERS, + "Invalid resource passed to create, " + + resource.name + " is not a service") + return false } } + return true + } + + /** + * Create an Application via XML. + * + * @return the Created application or null if Application creation + * failed. In that case the caller should use failureXml to determine + * the cause. + */ + private createApplication(xmlApplication) { + if (!validateApplicationServices(xmlApplication)) { + return null + } - def appName = xmlApplication[0].'@name' - def appLoc = xmlApplication[0].'@location' - def appDesc = xmlApplication[0].'@description' - def appEng = xmlApplication[0].'@engContact' - def appOps = xmlApplication[0].'@opsContact' - def appBiz = xmlApplication[0].'@bizContact' + def appName = xmlApplication.'@name' + def appLoc = xmlApplication.'@location' + def appDesc = xmlApplication.'@description' + def appEng = xmlApplication.'@engContact' + def appOps = xmlApplication.'@opsContact' + def appBiz = xmlApplication.'@bizContact' def applicationValue = new ApplicationValue() - applicationValue.name = appName - applicationValue.location = appLoc - applicationValue.description = appDesc - applicationValue.engContact = appEng - applicationValue.opsContact = appOps + applicationValue.name = appName + applicationValue.location = appLoc + applicationValue.description = appDesc + applicationValue.engContact = appEng + applicationValue.opsContact = appOps applicationValue.businessContact = appBiz - def newApp; - + def newApp try { applicationValue.applicationType = appMan.findApplicationType(1) - newApp = appMan.createApplication( user, applicationValue, new ArrayList()) + newApp = appMan.createApplication(user, applicationValue, new ArrayList()) // Initialize appServices to avoid NPE newApp.appServices = new ArrayList() } catch (AppdefDuplicateNameException e) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.OBJECT_EXISTS) - } - } - return + failureXml = getFailureXML(ErrorCode.OBJECT_EXISTS, + "Existing application with name " + appName + + "already exists.") + return null } catch (Exception e) { - renderXml() { - log.error("Error creating application", e) - ApplicationResponse() { - out << getFailureXML(ErrorCode.UNEXPECTED_ERROR) - } - } - return + log.error("Error creating application", e) + failureXml = getFailureXML(ErrorCode.UNEXPECTED_ERROR, + "Error creating application: " + e.message) + return null } def resources = xmlApplication['Resource'] updateAppServices(newApp, resources) - - renderXml() { - ApplicationResponse() { - out << getSuccessXML() - out << getApplicationXML(newApp.applicationValue) - } - } + return newApp } - def update(params) { - def updateRequest = new XmlParser().parseText(getUpload('postdata')) - def xmlApplication = updateRequest['Application'] + def create(params) { + def createRequest = new XmlParser().parseText(getUpload('postdata')) + def xmlApplication = createRequest['Application'] + def newApp if (!xmlApplication || xmlApplication.size() != 1) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS) + failureXml = getFailureXML(ErrorCode.INVALID_PARAMETERS, + "Wrong number of Applications") + } else { + newApp = createApplication(xmlApplication[0]) + } + + renderXml() { + ApplicationResponse() { + if (failureXml) { + out << failureXml + } else { + out << getSuccessXML() + out << getApplicationXML(newApp.applicationValue) } } - return } + } - def appId = xmlApplication[0].'@id'?.toInteger() + /** + * Update an Application via XML. + * + * @return the Created application or null if Application creation + * failed. In that case the caller should use failureXml to determine + * the cause. + */ + private updateApplication(xmlApplication) { + def appId = xmlApplication.'@id'?.toInteger() if (!appId) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS, - "No application id found") - } - } - return + failureXml = getFailureXML(ErrorCode.INVALID_PARAMETERS, + "No application id found") + return null } - // Validate Resources - for (xmlResource in xmlApplication['Resource']) { - def rid = xmlResource.'@id'?.toInteger() - def resource = resourceHelper.findById(rid) - if (!resource.isService()) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS, - "Invalid resource passed to create, " + - resource.name + " is not a service") - } - } - return - } + if (!validateApplicationServices(xmlApplication)) { + return null } - def appName = xmlApplication[0].'@name' - def appLoc = xmlApplication[0].'@location' - def appDesc = xmlApplication[0].'@description' - def appEng = xmlApplication[0].'@engContact' - def appOps = xmlApplication[0].'@opsContact' - def appBiz = xmlApplication[0].'@bizContact' + def appName = xmlApplication.'@name' + def appLoc = xmlApplication.'@location' + def appDesc = xmlApplication.'@description' + def appEng = xmlApplication.'@engContact' + def appOps = xmlApplication.'@opsContact' + def appBiz = xmlApplication.'@bizContact' def updateApp try { updateApp = appMan.findApplicationById(user, appId) } catch (Exception e) { log.error("Error finding application" + e) - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.OBJECT_NOT_FOUND) - } - } - return + failureXml = getFailureXML(ErrorCode.OBJECT_NOT_FOUND, + "Unable to find application with " + + "id " + appId) + return null } - + def applicationValue = updateApp.getApplicationValue() - applicationValue.name = appName - applicationValue.location = appLoc - applicationValue.description = appDesc - applicationValue.engContact = appEng - applicationValue.opsContact = appOps + applicationValue.name = appName + applicationValue.location = appLoc + applicationValue.description = appDesc + applicationValue.engContact = appEng + applicationValue.opsContact = appOps applicationValue.businessContact = appBiz try { appMan.updateApplication(user, applicationValue) } catch (AppdefDuplicateNameException e) { - renderXml() { - ApplicationResponse() { - out << getFailureXML(ErrorCode.INVALID_PARAMETERS, - "There is already an application named " + - appName) - } - } - return + failureXml = getFailureXML(ErrorCode.INVALID_PARAMETERS, + "There is already an application named " + + appName) + return null } catch (Exception e) { - renderXml() { - log.error("Error updating application", e) - ApplicationResponse() { - out << getFailureXML(ErrorCode.UNEXPECTED_ERROR) - } - } - return + log.error("Error updating application", e) + failureXml = getFailureXML(ErrorCode.UNEXPECTED_ERROR) + return null } def resources = xmlApplication['Resource'] updateAppServices(updateApp, resources) + return getApplication(appId) + } + + def update(params) { + def updateRequest = new XmlParser().parseText(getUpload('postdata')) + def xmlApplication = updateRequest['Application'] + + def updatedApp + if (!xmlApplication || xmlApplication.size() != 1) { + failureXml = getFailureXML(ErrorCode.INVALID_PARAMETERS, + "Wrong number of Applications") + } else { + updatedApp = updateApplication(xmlApplication[0]) + } renderXml() { ApplicationResponse() { - out << getSuccessXML() - // Must relookup the app to get updated services - out << getApplicationXML(applicationValue) + if (failureXml) { + out << failureXml + } else { + out << getSuccessXML() + out << getApplicationXML(updatedApp.applicationValue) + } + } + } + } + + def sync(params) { + def syncRequest = new XmlParser().parseText(getUpload('postdata')) + + def applications = [] + for (xmlApplication in syncRequest['Application']) { + def appId = xmlApplication.'@id'?.toInteger() + if (!appId) { + applications << createApplication(xmlApplication) + } else { + applications << updateApplication(xmlApplication) + } + + if (failureXml) { + // Break out early on errors. + break + } + } + + renderXml() { + ApplicationsResponse() { + if (failureXml) { + out << failureXml + } else { + out << getSuccessXML() + for (app in applications) { + out << getApplicationXML(app) + } + } } } } @@ -249,8 +277,6 @@ class ApplicationController extends ApiController { } def app = getApplication(id) - def failureXml = null - if (!app) { renderXml() { out << StatusResponse() { diff --git a/src/org/hyperic/hq/hqapi1/ApplicationApi.java b/src/org/hyperic/hq/hqapi1/ApplicationApi.java index 20120340..acf14bea 100644 --- a/src/org/hyperic/hq/hqapi1/ApplicationApi.java +++ b/src/org/hyperic/hq/hqapi1/ApplicationApi.java @@ -1,14 +1,11 @@ package org.hyperic.hq.hqapi1; -import org.hyperic.hq.hqapi1.types.ApplicationsResponse; -import org.hyperic.hq.hqapi1.types.ApplicationResponse; -import org.hyperic.hq.hqapi1.types.ApplicationRequest; -import org.hyperic.hq.hqapi1.types.Application; -import org.hyperic.hq.hqapi1.types.StatusResponse; +import org.hyperic.hq.hqapi1.types.*; import java.io.IOException; import java.util.Map; import java.util.HashMap; +import java.util.List; /** * The Hyperic HQ Application API. @@ -98,4 +95,23 @@ public StatusResponse deleteApplication(int id) params.put("id", new String[] { Integer.toString(id)}); return doGet("application/delete.hqu", params, StatusResponse.class); } + + /** + * Sync a list of {@link org.hyperic.hq.hqapi1.types.Application}s. + * + * @param applications The list of Applications to sync. + * + * @return On {@link org.hyperic.hq.hqapi1.types.ResponseStatus#SUCCESS}, + * the synced list of Application's are returned via + * {@link org.hyperic.hq.hqapi1.types.ApplicationsResponse#getApplication()}. + * + * @throws IOException If a network error occurs while making the request. + */ + public ApplicationsResponse syncApplications(List applications) + throws IOException { + + ApplicationsRequest applicationsRequest = new ApplicationsRequest(); + applicationsRequest.getApplication().addAll(applications); + return doPost("application/sync.hqu", applicationsRequest, ApplicationsResponse.class); + } } diff --git a/src/org/hyperic/hq/hqapi1/test/ApplicationSync_test.java b/src/org/hyperic/hq/hqapi1/test/ApplicationSync_test.java new file mode 100644 index 00000000..1b62bded --- /dev/null +++ b/src/org/hyperic/hq/hqapi1/test/ApplicationSync_test.java @@ -0,0 +1,198 @@ +package org.hyperic.hq.hqapi1.test; + +import org.hyperic.hq.hqapi1.HQApi; +import org.hyperic.hq.hqapi1.ApplicationApi; +import org.hyperic.hq.hqapi1.ResourceApi; +import org.hyperic.hq.hqapi1.types.Application; +import org.hyperic.hq.hqapi1.types.ApplicationsResponse; +import org.hyperic.hq.hqapi1.types.StatusResponse; +import org.hyperic.hq.hqapi1.types.ResourcePrototypeResponse; +import org.hyperic.hq.hqapi1.types.ResourcesResponse; + +import java.util.ArrayList; +import java.util.List; + +public class ApplicationSync_test extends ApplicationTestBase { + + public ApplicationSync_test(String name) { + super(name); + } + + public void testSyncCreate() throws Exception { + HQApi api = getApi(); + ApplicationApi appApi = api.getApplicationApi(); + + Application a = generateTestApplication(); + List apps = new ArrayList(); + apps.add(a); + + ApplicationsResponse response = appApi.syncApplications(apps); + hqAssertSuccess(response); + + assertEquals("Wrong number of applications returned by sync", 1, + response.getApplication().size()); + + Application syncedApp = response.getApplication().get(0); + + StatusResponse deleteResponse = appApi.deleteApplication(syncedApp.getId()); + hqAssertSuccess(deleteResponse); + } + + public void testSyncCreateMulti() throws Exception { + HQApi api = getApi(); + ApplicationApi appApi = api.getApplicationApi(); + + final int NUM = 10; + List apps = new ArrayList(); + for (int i = 0; i < NUM; i++) { + apps.add(generateTestApplication()); + } + + ApplicationsResponse response = appApi.syncApplications(apps); + hqAssertSuccess(response); + + assertEquals("Wrong number of applications returned by sync", NUM, + response.getApplication().size()); + + for (Application a : response.getApplication()) { + StatusResponse deleteResponse = appApi.deleteApplication(a.getId()); + hqAssertSuccess(deleteResponse); + } + } + + public void testSyncCreateMultiWithServices() throws Exception { + HQApi api = getApi(); + ResourceApi rApi = api.getResourceApi(); + ApplicationApi appApi = api.getApplicationApi(); + + ResourcePrototypeResponse protoResponse = + rApi.getResourcePrototype("CPU"); + hqAssertSuccess(protoResponse); + + ResourcesResponse cpusResponse = + rApi.getResources(protoResponse.getResourcePrototype(), + false, false); + hqAssertSuccess(cpusResponse); + + final int NUM = 10; + List apps = new ArrayList(); + for (int i = 0; i < NUM; i++) { + Application a = generateTestApplication(); + a.getResource().addAll(cpusResponse.getResource()); + apps.add(a); + } + + ApplicationsResponse response = appApi.syncApplications(apps); + hqAssertSuccess(response); + + assertEquals("Wrong number of applications returned by sync", NUM, + response.getApplication().size()); + + for (Application a : response.getApplication()) { + // Validate number of app services + assertEquals("Incorrect number of app services", + cpusResponse.getResource().size(), + a.getResource().size()); + + StatusResponse deleteResponse = appApi.deleteApplication(a.getId()); + hqAssertSuccess(deleteResponse); + } + } + + public void testSyncUpdate() throws Exception { + ApplicationApi api = getApi().getApplicationApi(); + + Application a = createTestApplication(null); + + a.setName(UPDATE_PREFIX + a.getName()); + a.setDescription(UPDATE_PREFIX + a.getDescription()); + a.setLocation(UPDATE_PREFIX + a.getLocation()); + a.setOpsContact(UPDATE_PREFIX + a.getOpsContact()); + a.setBizContact(UPDATE_PREFIX + a.getBizContact()); + a.setEngContact(UPDATE_PREFIX + a.getEngContact()); + + List apps = new ArrayList(); + apps.add(a); + + ApplicationsResponse syncResponse = api.syncApplications(apps); + hqAssertSuccess(syncResponse); + + assertEquals("Wrong number of applications returned by sync", 1, + syncResponse.getApplication().size()); + + Application updatedApp = syncResponse.getApplication().get(0); + + assertEquals(a.getName(), updatedApp.getName()); + assertEquals(a.getDescription(), updatedApp.getDescription()); + assertEquals(a.getLocation(), updatedApp.getLocation()); + assertEquals(a.getOpsContact(), updatedApp.getOpsContact()); + assertEquals(a.getBizContact(), updatedApp.getBizContact()); + assertEquals(a.getEngContact(), updatedApp.getEngContact()); + + StatusResponse deleteResponse = api.deleteApplication(updatedApp.getId()); + hqAssertSuccess(deleteResponse); + } + + public void testSyncUpdateMulti() throws Exception { + HQApi api = getApi(); + ResourceApi rApi = api.getResourceApi(); + ApplicationApi appApi = api.getApplicationApi(); + + ResourcePrototypeResponse protoResponse = + rApi.getResourcePrototype("CPU"); + hqAssertSuccess(protoResponse); + + ResourcesResponse cpusResponse = + rApi.getResources(protoResponse.getResourcePrototype(), + false, false); + hqAssertSuccess(cpusResponse); + + final int NUM = 10; + List apps = new ArrayList(); + + for (int i = 0; i < NUM; i++) { + Application a = createTestApplication(null); + a.setName(UPDATE_PREFIX + a.getName()); + a.setDescription(UPDATE_PREFIX + a.getDescription()); + a.setLocation(UPDATE_PREFIX + a.getLocation()); + a.setOpsContact(UPDATE_PREFIX + a.getOpsContact()); + a.setBizContact(UPDATE_PREFIX + a.getBizContact()); + a.setEngContact(UPDATE_PREFIX + a.getEngContact()); + a.getResource().addAll(cpusResponse.getResource()); + apps.add(a); + } + + ApplicationsResponse syncResponse = appApi.syncApplications(apps); + hqAssertSuccess(syncResponse); + + assertEquals("Wrong number of applications returned by sync", NUM, + syncResponse.getApplication().size()); + + for (Application a : syncResponse.getApplication()) { + assertTrue(a.getName().startsWith(UPDATE_PREFIX)); + assertTrue(a.getDescription().startsWith(UPDATE_PREFIX)); + assertTrue(a.getLocation().startsWith(UPDATE_PREFIX)); + assertTrue(a.getOpsContact().startsWith(UPDATE_PREFIX)); + assertTrue(a.getBizContact().startsWith(UPDATE_PREFIX)); + assertTrue(a.getEngContact().startsWith(UPDATE_PREFIX)); + + assertEquals("Invalid number of application services!", + cpusResponse.getResource().size(), + a.getResource().size()); + + StatusResponse deleteResponse = appApi.deleteApplication(a.getId()); + hqAssertSuccess(deleteResponse); + } + } + + public void testSyncEmpty() throws Exception { + ApplicationApi api = getApi().getApplicationApi(); + + List a = new ArrayList(); + ApplicationsResponse response = api.syncApplications(a); + hqAssertSuccess(response); + + assertEquals("Invalid number of Applications returned from sync", + 0, response.getApplication().size()); + } +} diff --git a/src/org/hyperic/hq/hqapi1/test/ApplicationTestBase.java b/src/org/hyperic/hq/hqapi1/test/ApplicationTestBase.java index 0ab510d8..57b391cb 100644 --- a/src/org/hyperic/hq/hqapi1/test/ApplicationTestBase.java +++ b/src/org/hyperic/hq/hqapi1/test/ApplicationTestBase.java @@ -10,6 +10,8 @@ public abstract class ApplicationTestBase extends HQApiTestBase { + static final String UPDATE_PREFIX = "UPDATED-"; + protected static final String APP_NAME = "Test Application"; protected static final String APP_LOCATION = "SFO"; protected static final String APP_DESC = "Test Application Description"; @@ -55,8 +57,6 @@ protected Application createTestApplication(List services) throws Exception { ApplicationApi api = getApi().getApplicationApi(); - - Random r = new Random(); Application a = generateTestApplication(); if (services != null) { diff --git a/src/org/hyperic/hq/hqapi1/test/ApplicationUpdate_test.java b/src/org/hyperic/hq/hqapi1/test/ApplicationUpdate_test.java index 048665bc..a401ecb6 100644 --- a/src/org/hyperic/hq/hqapi1/test/ApplicationUpdate_test.java +++ b/src/org/hyperic/hq/hqapi1/test/ApplicationUpdate_test.java @@ -17,8 +17,6 @@ public class ApplicationUpdate_test extends ApplicationTestBase { - private static final String UPDATE_PREFIX = "UPDATED-"; - public ApplicationUpdate_test(String name) { super(name); } diff --git a/src/org/hyperic/hq/hqapi1/tools/ApplicationCommand.java b/src/org/hyperic/hq/hqapi1/tools/ApplicationCommand.java index e305825d..80916d98 100644 --- a/src/org/hyperic/hq/hqapi1/tools/ApplicationCommand.java +++ b/src/org/hyperic/hq/hqapi1/tools/ApplicationCommand.java @@ -8,22 +8,19 @@ import org.hyperic.hq.hqapi1.types.*; import java.util.Arrays; +import java.util.List; +import java.io.InputStream; public class ApplicationCommand extends Command { private static String CMD_LIST = "list"; + private static String CMD_SYNC = "sync"; private static String CMD_DELETE = "delete"; - private static String[] COMMANDS = { CMD_LIST, CMD_DELETE }; + private static String[] COMMANDS = { CMD_LIST, CMD_SYNC, CMD_DELETE }; private static String OPT_ID = "id"; - - // Additional sync commands when syncing via command line options. -// private static String OPT_NAME = "name"; -// private static String OPT_PROTOTYPE = "prototype"; -// private static String OPT_REGEX = "regex"; -// private static String OPT_DELETEMISSING = "deleteMissing"; -// private static String OPT_DESC = "description"; + private static String OPT_BATCH_SIZE = "batchSize"; private void printUsage() { System.err.println("One of " + Arrays.toString(COMMANDS) + " required"); @@ -37,6 +34,8 @@ protected void handleCommand(String[] args) throws Exception { if (args[0].equals(CMD_LIST)) { list(trim(args)); + } else if (args[0].equals(CMD_SYNC)) { + sync(trim(args)); } else if (args[0].equals(CMD_DELETE)) { delete(trim(args)); } else { @@ -61,6 +60,46 @@ private void list(String[] args) throws Exception { XmlUtil.serialize(applications, System.out, Boolean.TRUE); } + private void sync(String[] args) throws Exception { + + OptionParser p = getOptionParser(); + + p.accepts(OPT_BATCH_SIZE, "Process the sync in batches of the given size"). + withRequiredArg().ofType(Integer.class); + + OptionSet options = getOptions(p, args); + + ApplicationApi api = getApi(options).getApplicationApi(); + + InputStream is = getInputStream(options); + ApplicationsResponse resp = XmlUtil.deserialize(ApplicationsResponse.class, is); + List applications = resp.getApplication(); + + int numSynced = 0; + if (options.has(OPT_BATCH_SIZE)) { + int batchSize = (Integer)options.valueOf(OPT_BATCH_SIZE); + int numBatches = (int)Math.ceil(applications.size()/((double)batchSize)); + + for (int i = 0; i < numBatches; i++) { + System.out.println("Syncing batch " + (i + 1) + " of " + numBatches); + int fromIndex = i * batchSize; + int toIndex = (fromIndex + batchSize) > applications.size() ? + applications.size() : (fromIndex + batchSize); + ApplicationsResponse syncResponse = + api.syncApplications(applications.subList(fromIndex, + toIndex)); + checkSuccess(syncResponse); + numSynced += (toIndex - fromIndex); + } + } else { + ApplicationsResponse syncResponse = api.syncApplications(applications); + checkSuccess(syncResponse); + numSynced = applications.size(); + } + + System.out.println("Successfully synced " + numSynced + " applications."); + } + private void delete(String[] args) throws Exception { OptionParser p = getOptionParser(); diff --git a/xsd/HQApi1.xsd b/xsd/HQApi1.xsd index e137df3f..c10e410b 100644 --- a/xsd/HQApi1.xsd +++ b/xsd/HQApi1.xsd @@ -1093,25 +1093,17 @@ - - - - - - - + + + - - - - - - - + + + @@ -1140,5 +1132,5 @@ - +