diff --git a/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt b/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt index 208ecc1bd..c04869ce4 100644 --- a/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt +++ b/ocs-ktc/src/test/kotlin/org/ostelco/prime/ocs/consumption/grpc/OcsGrpcServerTest.kt @@ -27,6 +27,7 @@ class OcsGrpcServerTest { fun `load test OCS using gRPC`() = runBlocking { // Add delay to DB call and skip analytics and low balance notification + @ExperimentalUnsignedTypes OnlineCharging.loadUnitTest = true server = OcsGrpcServer(8082, OcsGrpcService(OnlineCharging)) diff --git a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt index a979a1a38..43d0a47ad 100644 --- a/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt +++ b/sim-administration/es2plus4dropwizard/src/main/kotlin/org/ostelco/sim/es2plus/Es2PlusClient.kt @@ -119,7 +119,9 @@ class ES2PlusClient( return response } - /* For test cases where content should be returned. */ + // + // For test cases where content should be returned. + // @Throws(ES2PlusClientException::class) private fun postEs2ProtocolCmd( path: String, @@ -231,7 +233,6 @@ class ES2PlusClient( } - fun confirmOrder(eid: String? = null, iccid: String, matchingId: String? = null, @@ -255,7 +256,13 @@ class ES2PlusClient( returnValueClass = Es2ConfirmOrderResponse::class.java) } - fun cancelOrder(iccid: String, finalProfileStatusIndicator: String, eid: String? = null, matchingId: String? = null): HeaderOnlyResponse { + /** + * Transmit a cancelOrder request for a particular ICCID. + */ + fun cancelOrder(iccid: String, + finalProfileStatusIndicator: String, + eid: String? = null, + matchingId: String? = null): HeaderOnlyResponse { return postEs2ProtocolCmd("/gsma/rsp2/es2plus/cancelOrder", es2ProtocolPayload = Es2CancelOrder( header = ES2RequestHeader( diff --git a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt index 34f775757..d11c41bd6 100644 --- a/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt +++ b/sim-administration/simmanager/src/integration-test/kotlin/org/ostelco/simcards/admin/SimAdministrationTest.kt @@ -2,15 +2,18 @@ package org.ostelco.simcards.admin import arrow.core.Either import com.codahale.metrics.health.HealthCheck +import com.google.common.collect.ImmutableMultimap import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser import io.dropwizard.client.HttpClientBuilder import io.dropwizard.client.JerseyClientBuilder import io.dropwizard.jdbi3.JdbiFactory +import io.dropwizard.servlets.tasks.Task import io.dropwizard.testing.ConfigOverride import io.dropwizard.testing.ResourceHelpers import io.dropwizard.testing.junit.DropwizardAppRule +import org.apache.http.impl.client.CloseableHttpClient import org.assertj.core.api.Assertions.assertThat import org.glassfish.jersey.client.ClientProperties import org.jdbi.v3.core.Jdbi @@ -30,15 +33,21 @@ import org.ostelco.simcards.hss.SimManagerToHssDispatcherAdapter import org.ostelco.simcards.inventory.HssState import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry +import org.ostelco.simcards.inventory.SimInventoryApi +import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimProfileKeyStatistics +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import org.ostelco.simcards.smdpplus.SmDpPlusApplication import org.testcontainers.containers.BindMode import org.testcontainers.containers.FixedHostPortGenericContainer import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy import java.io.FileInputStream +import java.io.PrintWriter +import java.io.StringWriter import java.time.Duration import java.time.temporal.ChronoUnit +import java.util.* import javax.ws.rs.client.Client import javax.ws.rs.client.Entity import javax.ws.rs.core.MediaType @@ -51,7 +60,6 @@ class SimAdministrationTest { private val phoneType = "rababara" private val expectedProfile = "IPHONE_PROFILE_2" - companion object { private lateinit var jdbi: Jdbi private lateinit var client: Client @@ -168,7 +176,6 @@ class SimAdministrationTest { dao.clearTables() } - private fun presetTables() { val dao = SIM_MANAGER_RULE.getApplication().getDAO() @@ -178,7 +185,7 @@ class SimAdministrationTest { } /* The SIM dataset is the same that is used by the SM-DP+ emulator. */ - private fun loadSimData(hssState: HssState? = null, queryParameterName: String = "initialHssState", expectedReturnCode: Int = 200) { + protected fun loadSimData(hssState: HssState? = null, queryParameterName: String = "initialHssState", expectedReturnCode: Int = 200) { val entries = FileInputStream(SM_DP_PLUS_RULE.configuration.simBatchData) var target = client.target("$simManagerEndpoint/$hssName/import-batch/profilevendor/$profileVendor") if (hssState != null) { @@ -186,9 +193,7 @@ class SimAdministrationTest { } val response = - target - .request() - .put(Entity.entity(entries, MediaType.TEXT_PLAIN)) + target.request().put(Entity.entity(entries, MediaType.TEXT_PLAIN)) assertThat(response.status).isEqualTo(expectedReturnCode) } @@ -303,64 +308,191 @@ class SimAdministrationTest { } } - @Test - fun testPeriodicProvisioningTask() { - loadSimData() - val simDao = SIM_MANAGER_RULE.getApplication() - .getDAO() + /** + * Helper class that is used to wrap invocations of the + * preallocation and polling of outstandng profiles tasks. + */ + class TaskInvocationFixture(val testClass: SimAdministrationTest) { + var simDao: SimInventoryDAO + var profileVendors: List + var hssConfigs: List + var httpClient : CloseableHttpClient + var maxNoOfProfilesToAllocate = 10 + var dispatcher: DirectHssDispatcher + var hssAdapterCache: SimManagerToHssDispatcherAdapter + var preStats: SimProfileKeyStatistics + var pvaf: ProfileVendorAdapterFactory + var preallocationTask: PreallocateProfilesTask - val profileVendors = SIM_MANAGER_RULE.configuration.profileVendors - val hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors - val httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) - .build("periodicProvisioningTaskClient") - val maxNoOfProfilesToAllocate = 10 + val simApi: SimInventoryApi - val hlrs = simDao.getHssEntries() - assertThat(hlrs.isRight()).isTrue() + val pollingTask : PollOutstandingProfilesTask - var hssId: Long = 0 - hlrs.map { - hssId = it[0].id + var hssId: Long = -1L + + init { + testClass.loadSimData() + + simDao = SIM_MANAGER_RULE.getApplication() + .getDAO() + + profileVendors = SIM_MANAGER_RULE.configuration.profileVendors + hssConfigs = SIM_MANAGER_RULE.configuration.hssVendors + httpClient = HttpClientBuilder(SIM_MANAGER_RULE.environment) + .build("taskInvocationFixtureClient-${UUID.randomUUID()}") + + val hlrs = simDao.getHssEntries() + assertThat(hlrs.isRight()).isTrue() + + hlrs.map { + hssId = it[0].id + } + + dispatcher = DirectHssDispatcher( + hssConfigs = hssConfigs, + httpClient = httpClient, + healthCheckRegistrar = object : HealthCheckRegistrar { + override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { + SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) + } + }) + hssAdapterCache = SimManagerToHssDispatcherAdapter( + dispatcher = dispatcher, + simInventoryDAO = simDao) + preStats = SimProfileKeyStatistics( + noOfEntries = 0L, + noOfEntriesAvailableForImmediateUse = 0L, + noOfReleasedEntries = 0L, + noOfUnallocatedEntries = 0L, + noOfReservedEntries = 0L) + pvaf = ProfileVendorAdapterFactory( + simInventoryDAO = simDao, + httpClient = httpClient, + profileVendors = ConfigRegistry.config.profileVendors) + preallocationTask = PreallocateProfilesTask( + maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, + simInventoryDAO = simDao, + hssAdapterProxy = hssAdapterCache, + pvaf = pvaf) + simApi = SimInventoryApi(httpClient, SIM_MANAGER_RULE.configuration, simDao) + + pollingTask = PollOutstandingProfilesTask(simInventoryDAO = simDao, pvaf = pvaf) + } + + + + private fun executeTask(task: Task): String { + val out = StringWriter(); + val writer = PrintWriter(out); + val parameters = ImmutableMultimap.builder().build() + task.execute(parameters, writer) + return out.toString() + } + + /** + * Execute the preallocation task, returning the report it makes as a string. + */ + fun executePreallocateSimProfilesTask(): String { + return executeTask(preallocationTask) + } + + /** + * Execute the polling of outstanding sim roles task, returning the report it makes as a string. + */ + fun executePollOutstandingSimrofilesTask() : String { + return executeTask(pollingTask) } - val dispatcher = DirectHssDispatcher( - hssConfigs = hssConfigs, - httpClient = httpClient, - healthCheckRegistrar = object : HealthCheckRegistrar { - override fun registerHealthCheck(name: String, healthCheck: HealthCheck) { - SIM_MANAGER_RULE.environment.healthChecks().register(name, healthCheck) - } - }) - val hssAdapterCache = SimManagerToHssDispatcherAdapter( - dispatcher = dispatcher, - simInventoryDAO = simDao) - val preStats = SimProfileKeyStatistics( - noOfEntries = 0L, - noOfEntriesAvailableForImmediateUse = 0L, - noOfReleasedEntries = 0L, - noOfUnallocatedEntries = 0L, - noOfReservedEntries = 0L) - val task = PreallocateProfilesTask( - profileVendors = profileVendors, - simInventoryDAO = simDao, - maxNoOfProfileToAllocate = maxNoOfProfilesToAllocate, - hssAdapterProxy = hssAdapterCache, - httpClient = httpClient) - task.preAllocateSimProfiles() - - val postAllocationStats = - simDao.getProfileStats(hssId, expectedProfile) - assertThat(postAllocationStats.isRight()).isTrue() - - var postStats = SimProfileKeyStatistics(0L, 0L, 0L, 0L, 0L) - postAllocationStats.map { - postStats = it + /** + * Allocate the next sim profile for a generic ("nokia") phone. This method + * doesn't actually use the tasks, but it is used as a convenience methods when + * testing the tasks. + */ + fun allocateNextEsimProfile ():SimEntry { + val simEntry = simApi.allocateNextEsimProfile(hssName = testClass.hssName, phoneType = "nokia") + .fold({ null }, { it }) + + assertNotNull(simEntry) + return simEntry!! } + /** + * Switch off callbacks from the SM-DP+ emulator to the Prime emulator. + */ + fun disableEs2CallbacksFromSmdpPlus() { + SM_DP_PLUS_RULE.getApplication().disableCallbacks() + } + + /** + * Instruct the SM-DP+ emulator to simulate an installation of a simcard. + */ + fun emulateDownloadOfIccid(iccid: String) { + SM_DP_PLUS_RULE.getApplication().emulateInstallOfIccid(iccid) + } + + + /** + * Get statistics for a specific type of sim profile. Used to test + * if the preallocation task is doing the right thing. + */ + fun getStatsForProfile(profile:String): SimProfileKeyStatistics { + val postAllocationStats = + simDao.getProfileStats(hssId, profile) + assertThat(postAllocationStats.isRight()).isTrue() + + var stats : SimProfileKeyStatistics? = null + postAllocationStats.map { + stats = it + } + if (stats == null) { + fail("Failed to get stats for profile $profile") + } + + return stats!! + } + } + + + @Test + fun testPollingOfOutstandingProfilesTask() { + val tif = TaskInvocationFixture(this) + + tif.executePreallocateSimProfilesTask() + + // Run the polling, and observe that nothing happens. + // Then run the polling task and see what happens. + var outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(true) // TODO: Replace with something more useful + + // Then allocate the next profile + val simEntry = tif.allocateNextEsimProfile() + + // Then run the polling task and see what happens. + outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(outString.contains("State for iccid=${simEntry.iccid} still set to RELEASED")) + + // Disable callbacks and emulate dowload (emulation of failing callbacks from SMDP+, which + // is the situation we want the callbacks to help us with) + tif.disableEs2CallbacksFromSmdpPlus() + tif.emulateDownloadOfIccid(simEntry.iccid) + + outString = tif.executePollOutstandingSimrofilesTask() + assertTrue(outString.contains("Updated state for iccid=${simEntry.iccid} to INSTALLED")) + } + + @Test + fun testPeriodicProvisioningTask() { + val tif = TaskInvocationFixture(this) + + val preStats = tif.getStatsForProfile(expectedProfile) + tif.executePreallocateSimProfilesTask() + + val postStats = tif.getStatsForProfile(expectedProfile) + val noOfAllocatedProfiles = postStats.noOfEntriesAvailableForImmediateUse - preStats.noOfEntriesAvailableForImmediateUse assertEquals( - maxNoOfProfilesToAllocate.toLong(), + tif.maxNoOfProfilesToAllocate.toLong(), noOfAllocatedProfiles) } @@ -491,7 +623,6 @@ class SimAdministrationTest { assertGaugeValue(2, "sims.noOfReservedEntries.IPHONE_PROFILE_2") } - // XXX MISSING TEST: SHould test periodic updater also in cases where // either HSS or SM-DP+ entries are pre-allocated. diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt index 518b60668..84c037ba4 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PeriodicProvisioningTask.kt @@ -6,7 +6,6 @@ import arrow.core.left import arrow.core.right import com.google.common.collect.ImmutableMultimap import io.dropwizard.servlets.tasks.Task -import org.apache.http.impl.client.CloseableHttpClient import org.ostelco.prime.getLogger import org.ostelco.prime.jsonmapper.asJson import org.ostelco.prime.simmanager.AdapterError @@ -20,7 +19,7 @@ import org.ostelco.simcards.inventory.ProvisionState import org.ostelco.simcards.inventory.SimEntry import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimProfileKeyStatistics -import org.ostelco.simcards.profilevendors.ProfileVendorAdapter +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory import java.io.PrintWriter import kotlin.math.min @@ -41,15 +40,12 @@ import kotlin.math.min class PreallocateProfilesTask( private val lowWaterMark: Int = 10, val maxNoOfProfileToAllocate: Int = 30, + val pvaf : ProfileVendorAdapterFactory, val simInventoryDAO: SimInventoryDAO, - val httpClient: CloseableHttpClient, - val hssAdapterProxy: SimManagerToHssDispatcherAdapter, - val profileVendors: List) : Task("preallocate_sim_profiles") { - + val hssAdapterProxy: SimManagerToHssDispatcherAdapter) : Task("preallocate_sim_profiles") { private val logger by getLogger() - @Throws(Exception::class) override fun execute(parameters: ImmutableMultimap, output: PrintWriter) { // TODO: Rewrite to deliver a report of what happened, also if things went well. @@ -60,30 +56,13 @@ class PreallocateProfilesTask( } } - private fun getConfigForVendorNamed(name: String) = - profileVendors.firstOrNull { - it.name == name - } - - private fun getProfileVendorAdapterForProfileVendorId(profileVendorId: Long): Either = - simInventoryDAO.getProfileVendorAdapterDatumById(profileVendorId) - .flatMap { datum -> - val profileVendorConfig = getConfigForVendorNamed(datum.name) - if (profileVendorConfig == null) { - AdapterError("profileVendorCondig null for profile vendor $profileVendorId, that's very bad.").left() - } else { - ProfileVendorAdapter(datum, profileVendorConfig, httpClient, simInventoryDAO).right() - } - } - - // TODO: This method must be refactored. It is still _way_ too complex. private fun preProvisionSimProfile(hssEntry: HssEntry, simEntry: SimEntry): Either = if (simEntry.id == null) { // TODO: This idiom is _bad_, find something better! AdapterError("simEntry.id == null for simEntry = '$simEntry'.").left() } else - getProfileVendorAdapterForProfileVendorId(simEntry.profileVendorId) + pvaf.getAdapterByVendorId(simEntry.profileVendorId) .flatMap { profileVendorAdapter -> when { simEntry.hssState == HssState.NOT_ACTIVATED -> { diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt new file mode 100644 index 000000000..b2dae1737 --- /dev/null +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/PollOutstandingProfilesTask.kt @@ -0,0 +1,135 @@ +package org.ostelco.simcards.admin + +import arrow.core.Either +import com.google.common.collect.ImmutableMultimap +import io.dropwizard.servlets.tasks.Task +import org.ostelco.prime.getLogger +import org.ostelco.prime.jsonmapper.asJson +import org.ostelco.prime.simmanager.SimManagerError +import org.ostelco.sim.es2plus.ProfileStatus +import org.ostelco.simcards.inventory.SimEntry +import org.ostelco.simcards.inventory.SimInventoryDAO +import org.ostelco.simcards.inventory.SmDpPlusState +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory +import java.io.PrintWriter + +/** + * A dropwizard "task" that is intended to be invoked as an administrative step + * by an external agent that is part of the serving system, not a customer of it. + * + * The task implements a task that for all of the sim profiles in storage + * will search for profiles that has been allocated to end user equipment + * but has not yet been downloaded or installed by it, as seen by ES2+ + * callbacks. + * + * The functionality is duplicating some of the functions implemented by + * the ES2+ callback mechanism. The reason why we need this task in addition + * is that we have found the ES2+ callback to be unreliable. This task + * is therefore intended to serve as a fallback for that when working correctly + * preferable mechanism. + */ + +class PollOutstandingProfilesTask( + val simInventoryDAO: SimInventoryDAO, + val pvaf: ProfileVendorAdapterFactory) : Task("poll_outstanding_profiles") { + + private val logger by getLogger() + + @Throws(Exception::class) + override fun execute(parameters: ImmutableMultimap, output: PrintWriter) { + // TODO: Rewrite to deliver a report of what happened, also if things went well. + pollAllocatedButNotDownloadedProfiles(output) + .mapLeft { simManagerError -> + logger.error(simManagerError.description) + output.println(asJson(simManagerError)) + } + } + + private fun pollAllocatedButNotDownloadedProfiles(output: PrintWriter): Either { + return simInventoryDAO.findAllocatedButNotDownloadedProfiles().map { profilesToPoll -> + pollForSmdpStatus(output, profilesToPoll) + } + } + + private fun pollForSmdpStatus(output: PrintWriter, profilesToPoll: List) { + + val result = StringBuffer() + + @Synchronized + fun reportln(s: String, e: Exception? = null) { + output.println(s) + if (e != null) { + output.println(" Caused exception: $e") + } + } + + fun sendReport() { + output.print(result.toString()) + } + + fun updateProfileInDb(p: ProfileStatus) { + try { + + // Get state and iccid non null strings + val stateString = p.state ?: "" + val iccid = p.iccid ?: "" + + // Then discard empty states. + if (stateString == "") { + reportln("Empty state detected for iccid ${p.iccid}") + return + } + + val state = SmDpPlusState.valueOf(stateString.toUpperCase()) + + if (state != SmDpPlusState.RELEASED) { + val update = simInventoryDAO.setSmDpPlusStateUsingIccid(iccid, state) + if (update.isLeft()) { // TODO: This is is not idiomatic. Please help me make it idomatic before merging to develop. + update.mapLeft { + reportln("Could not update iccid=$iccid still set to ${state.name}. Sim manager error = $it") + } + } else { + // This probably represents an error situation in the SM-DP+, and _should_ perhaps + // be reported as an error by the sim manager. Please think about this and + // either change it to logger.error yourself, or ask me in review comments to do it for you. + val report = "Updated state for iccid=$iccid to ${state.name}" + logger.info(report) + reportln(report) + } + } else { + reportln("State for iccid=$iccid still set to ${state.name}") + } + } catch (e: Exception) { + reportln("Couldn't update status for iccid ${p.iccid}", e) + } + } + + + fun asVendorIdToIccdList(profiles: List): Map> = + profiles.map { + mapOf(it.profileVendorId to it.iccid) + }.flatMap { + it.entries + }.groupBy { + it.key + }.mapValues { vendor -> + vendor.value.map { it.value } + } + + + // Then poll them individually via the profile vendor adapter associated with the + // profile (there is not much to be gained here by doing it in paralellel, but perhaps + // one day it will be). + + for ((vendorId, iccidList) in asVendorIdToIccdList(profilesToPoll)) { + pvaf.getAdapterByVendorId(vendorId).mapRight { profileVendorAdapter -> + val statuses = + profileVendorAdapter.getProfileStatusList(iccidList) + statuses.mapRight { + it.forEach { updateProfileInDb(it) } + } + } // TODO: Am I missing the error situation here? + } + } +} + diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt index f249697ba..c10998792 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimAdministrationModule.kt @@ -30,6 +30,7 @@ import org.ostelco.simcards.inventory.SimInventoryDAO import org.ostelco.simcards.inventory.SimInventoryDB import org.ostelco.simcards.inventory.SimInventoryDBWrapperImpl import org.ostelco.simcards.inventory.SimInventoryResource +import org.ostelco.simcards.profilevendors.ProfileVendorAdapterFactory /** * The SIM manager @@ -108,12 +109,20 @@ class SimAdministrationModule : PrimeModule { simInventoryDAO = this.DAO ) - env.admin().addTask(PreallocateProfilesTask( + val pvaf = ProfileVendorAdapterFactory( simInventoryDAO = this.DAO, httpClient = httpClient, + profileVendors = config.profileVendors + ) + + env.admin().addTask(PreallocateProfilesTask( + simInventoryDAO = this.DAO, hssAdapterProxy = hssAdapters, - profileVendors = config.profileVendors)) + pvaf = pvaf)) + env.admin().addTask(PollOutstandingProfilesTask( + simInventoryDAO = this.DAO, + pvaf = pvaf)) env.healthChecks().register("smdp", SmdpPlusHealthceck(getDAO(), httpClient, config.profileVendors)) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt index eb3a9aeb8..262337b09 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SimManager.kt @@ -15,10 +15,10 @@ object SimManagerSingleton : SimManager { private val logger by getLogger() - override fun allocateNextEsimProfile(hlr: String, phoneType: String?): Either = - simInventoryApi.allocateNextEsimProfile(hlrName = hlr, phoneType = "$hlr.${phoneType ?: "generic"}").bimap( + override fun allocateNextEsimProfile(hssName: String, phoneType: String?): Either = + simInventoryApi.allocateNextEsimProfile(hssName = hssName, phoneType = "$hssName.${phoneType ?: "generic"}").bimap( { - "Failed to allocate eSIM for HLR - $hlr for phoneType - $phoneType" + "Failed to allocate eSIM for HLR - $hssName for phoneType - $phoneType" }, { simEntry -> mapToModelSimEntry(simEntry) }) diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt index 2b11816f7..429b6bb0d 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryApi.kt @@ -65,14 +65,14 @@ class SimInventoryApi(private val httpClient: CloseableHttpClient, } } - fun allocateNextEsimProfile(hlrName: String, phoneType: String): Either = + fun allocateNextEsimProfile(hssName: String, phoneType: String): Either = IO { Either.monad().binding { - logger.info("Allocating new SIM for hlr ${hlrName} and phone-type ${phoneType}") + logger.info("Allocating new SIM for hlr ${hssName} and phone-type ${phoneType}") - val hlrAdapter = dao.getHssEntryByName(hlrName) + val hlrAdapter = dao.getHssEntryByName(hssName) .bind() - val profile = getProfileType(hlrName, phoneType) + val profile = getProfileType(hssName, phoneType) .bind() val simEntry = dao.findNextReadyToUseSimProfileForHss(hlrAdapter.id, profile) .bind() diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt index d715620a2..c20335900 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDB.kt @@ -234,7 +234,6 @@ interface SimInventoryDB { fun getProfileNamesForHss(hssId: Long): List - // WHERE hlrId = 2 AND profile = 'OYA_M1_BF76' AND // smdpPlusState <> 'DOWNLOADED' AND smdpPlusState <> 'INSTALLED' AND smdpPlusState <> 'ENABLED' AND provisionState = 'AVAILABLE' AND matchingid is null @@ -283,7 +282,6 @@ interface SimInventoryDB { provisionedAvailableState: String = ProvisionState.AVAILABLE.name): List - @SqlQuery(""" SELECT DISTINCT profile AS simprofilename, hlrid AS hssid, hlr_adapters.name AS hssname FROM sim_entries, hlr_adapters WHERE hlrid=hlr_adapters.id """) @@ -291,6 +289,22 @@ interface SimInventoryDB { fun getHssProfileNamePairs(): List + /** + * Row mapper for the getHssProfileNamePairs method. + */ + class HssProfileNameMapper : RowMapper { + override fun map(row: ResultSet, ctx: StatementContext): HssProfileIdName? { + if (row.isAfterLast) { + return null + } + + val hssId = row.getLong("hssid") + val hssName = row.getString("hssname") + val simProfileName = row.getString("simprofilename") + return HssProfileIdName(hssId = hssId, hssName = hssName, simProfileName = simProfileName) + } + } + /** * Golden numbers are numbers ending in either "0000" or "9999", and they have to be * treated specially. @@ -299,18 +313,14 @@ interface SimInventoryDB { WHERE batch = :batchId AND msisdn ~ '[0-9]*(0000|9999)$' """) fun reserveGoldenNumbersForBatch(batchId: Long, provisionReservedState: ProvisionState = ProvisionState.RESERVED): Int -} - -class HssProfileNameMapper : RowMapper { - override fun map(row: ResultSet, ctx: StatementContext): HssProfileIdName? { - if (row.isAfterLast) { - return null - } - val hssId = row.getLong("hssid") - val hssName = row.getString("hssname") - val simProfileName = row.getString("simprofilename") - return HssProfileIdName(hssId = hssId, hssName = hssName, simProfileName = simProfileName) - } + /** + * Return a list of all sim profiles that are both priovisioned for a specific + * end user equipment, and still in SMDP sttr 'RELEASED' meaning that it hasn't + * been downloaded, installed or deleted yet. + */ + @SqlQuery("""SELECT * FROM sim_entries + WHERE provisionstate = 'PROVISIONED' and smdpplusstate = 'RELEASED'""") + fun findAllocatedButNotDownloadedProfiles(): List } diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt index 71369799b..ee839a44c 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapper.kt @@ -152,10 +152,15 @@ interface SimInventoryDBWrapper { fun reserveGoldenNumbersForBatch(batchId: Long): Either /** - * Return a list of sim Profile names associated with HSSes. Return both the - * HSSId (database internal ID), and the public name of the HSS. + * Return a list of (HSS ID, hss Name, sim profile name) tuples. */ fun getHssProfileNamePairs(): Either> + + /** + * Return a list of sim entries representing profiles that has been allocated + * to end user equipment, but has not + */ + fun findAllocatedButNotDownloadedProfiles(): Either> } /** diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt index d7f368c3d..dde2cf44e 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/inventory/SimInventoryDBWrapperImpl.kt @@ -60,6 +60,11 @@ class SimInventoryDBWrapperImpl(private val db: SimInventoryDB) : SimInventoryDB db.findNextReadyToUseSimProfileForHlr(hssId, profile) } + override fun findAllocatedButNotDownloadedProfiles(): Either> = + either(NotFoundError("Failure while getting allocated but nt downloaded profiles")) { + db.findAllocatedButNotDownloadedProfiles() + } + @Transaction override fun setEidOfSimProfileByIccid(iccid: String, eid: String): Either = either(NotFoundError("Found no SIM profile with ICCID $iccid update of EID failed")) { diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt index 8fdc404fe..89030fe9a 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt @@ -25,6 +25,29 @@ import org.ostelco.simcards.inventory.SmDpPlusState import java.net.URL +class ProfileVendorAdapterFactory( + val simInventoryDAO: SimInventoryDAO, + val httpClient: CloseableHttpClient, + val profileVendors: List) { + + // Give this a shorter name + fun getAdapterByVendorId(profileVendorId: Long): Either = + simInventoryDAO.getProfileVendorAdapterDatumById(profileVendorId) + .flatMap { datum -> + val profileVendorConfig = getConfigForVendorNamed(datum.name) + if (profileVendorConfig == null) { + AdapterError("profileVendorCondig null for profile vendor $profileVendorId, that's very bad.").left() + } else { + ProfileVendorAdapter(datum, profileVendorConfig, httpClient, simInventoryDAO).right() + } + } + + private fun getConfigForVendorNamed(name: String) = + profileVendors.firstOrNull { + it.name == name + } +} + // TODO: Why on earth is the json property set to "metricName"? It makes no sense. // Fix it, but understand what it means. data class ProfileVendorAdapterDatum( @@ -226,9 +249,8 @@ data class ProfileVendorAdapter( /** * Downloads the SM-DP+ 'profile status' information for a list of ICCIDs * from a SM-DP+ service. - * @param httpClient HTTP client - * @param config SIM vendor specific configuration * @param iccidList list with ICCID + * @param expectSuccess True iff no null values are to be expected. * @return A list with SM-DP+ 'profile status' information */ private fun getProfileStatus( @@ -244,6 +266,16 @@ data class ProfileVendorAdapter( } + + /** + * Return a list of profile statuses for a parameter list of profiles. Signal an error + * if an attempt is made at getting the status of a profile for which there is no known + * record + */ + fun getProfileStatusList(iccidList: List): Either> { + return getProfileStatus(iccidList = iccidList, expectSuccess = true) + } + /// /// Activating a sim card. /// @@ -278,4 +310,6 @@ data class ProfileVendorAdapter( */ fun ping(): Either> = getProfileStatus(iccidList = listContainingOnlyInvalidIccid, expectSuccess = false) + + } \ No newline at end of file diff --git a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt index fdf6b0290..9cff14cfa 100644 --- a/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt +++ b/sim-administration/sm-dp-plus-emulator/src/main/kotlin/org/ostelco/simcards/smdpplus/SmDpPlusApplication.kt @@ -81,6 +81,18 @@ class SmDpPlusApplication : Application() { fun getHttpClient() = httpClient + lateinit var callbackClient:SmDpPlusCallbackClient + + var callbacksEnabled = true + + fun enableCallbacks() { + callbacksEnabled = true + } + + fun disableCallbacks() { + callbacksEnabled = false + } + override fun run(config: SmDpPlusAppConfiguration, env: Environment) { @@ -106,7 +118,7 @@ class SmDpPlusApplication : Application() { } val simEntriesIterator = SmDpSimEntryIterator(FileInputStream(config.simBatchData)) - val smDpPlusEmulator = SmDpPlusEmulator(simEntriesIterator) + val smDpPlusEmulator = SmDpPlusEmulator(simEntriesIterator, this) this.smdpPlusService = smDpPlusEmulator this.serverResource = SmDpPlusServerResource( @@ -118,15 +130,14 @@ class SmDpPlusApplication : Application() { certConfig = config.certConfig))) */ - val callbackClient = SmDpPlusCallbackClient( + callbackClient = SmDpPlusCallbackClient( httpClient = httpClient, - hostname = config.es2plusConfig.host, portNumber = config.es2plusConfig.port, requesterId = config.es2plusConfig.requesterId, smdpPlus = smdpPlusService) - val commandsProcessor = CommandsProcessorResource(callbackClient) + val commandsProcessor = CommandsProcessorResource(this) jerseyEnvironment.register(commandsProcessor) // XXX This is weird, is it even necessary? Probably not. @@ -147,6 +158,21 @@ class SmDpPlusApplication : Application() { fun reset() { this.smdpPlusService.reset(); + enableCallbacks() + } + + fun setCurrentState(iccid: String, newState: String) { + if (newState == "DOWNLOAD" && callbacksEnabled) { // TODO: Add flag to switch reporting off (to test error situations) + callbackClient.reportDownload(iccid) + } + } + + fun reportDownload(iccid: String) { + setCurrentState(iccid, "DOWNLOAD") + } + + fun emulateInstallOfIccid(iccid: String) { + smdpPlusService.emulateInstallOfIccid(iccid) } } @@ -194,13 +220,12 @@ class SmDpPlusCallbackClient( * that has been registred to receive the callbacks. */ @Path("commands") -class CommandsProcessorResource(private val callbackClient: SmDpPlusCallbackClient) { - +class CommandsProcessorResource(private val app: SmDpPlusApplication) { @Path("simulate-download-of/iccid/{iccid}") @GET fun simulateDownloadOf(@PathParam("iccid") iccid: String): String { - callbackClient.reportDownload(iccid = iccid) + app.reportDownload(iccid) return "Simulated download of iccid ${iccid} went well." } } @@ -210,7 +235,7 @@ class CommandsProcessorResource(private val callbackClient: SmDpPlusCallbackClie * happy day scenarios, and not particulary efficient, and in-memory * only etc. */ -class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusService { +class SmDpPlusEmulator(incomingEntries: Iterator, val app: SmDpPlusApplication) : SmDpPlusService { private val log = LoggerFactory.getLogger(javaClass) @@ -259,6 +284,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic fun reset() { + app.enableCallbacks() entries.clear() entriesByIccid.clear() entriesByProfile.clear() @@ -285,6 +311,16 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic fun getEntryByIccid(iccid: String): SmDpSimEntry? = entriesByIccid[iccid] + fun setEntryState(entry: SmDpSimEntry, newState: String) { + entry.setCurrentState(newState) + app.setCurrentState(entry.iccid, newState) + } + + fun setEntryStateByIccid(iccid: String, state: String) { + val entry: SmDpSimEntry = getEntryByIccid(iccid) + ?: throw SmDpPlusException("Could not find download order matching criteria") + setEntryState(entry, state) + } // TODO; What about the reservation flag? override fun downloadOrder(eid: String?, iccid: String?, profileType: String?): Es2DownloadOrderResponse { @@ -300,7 +336,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic // Then mark the entry as allocated and return the corresponding ICCID. entry.allocated = true - entry.setCurrentState("DOWNLOADED") + setEntryState(entry, "DOWNLOADED") // Finally return the ICCID uniquely identifying the profile instance. return Es2DownloadOrderResponse(eS2SuccessResponseHeader(), @@ -390,7 +426,7 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic // XXX The state mechanism in this class is a pice of .... Fix it! entry.released = releaseFlag - entry.setCurrentState("RELEASED") + setEntryState(entry, "RELEASED") if (confirmationCode != null) { @@ -433,6 +469,10 @@ class SmDpPlusEmulator(incomingEntries: Iterator) : SmDpPlusServic override fun releaseProfile(iccid: String) { TODO("not implemented") } + + fun emulateInstallOfIccid(iccid: String) { + setEntryStateByIccid(iccid, "INSTALLED") + } } /**