Improved error reporting if failed to start
[aquarium] / src / main / scala / gr / grnet / aquarium / Configurator.scala
1 /*
2  * Copyright 2011-2012 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
12  *   2. Redistributions in binary form must reproduce the above
13  *      copyright notice, this list of conditions and the following
14  *      disclaimer in the documentation and/or other materials
15  *      provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and
31  * documentation are those of the authors and should not be
32  * interpreted as representing official policies, either expressed
33  * or implied, of GRNET S.A.
34  */
35
36 package gr.grnet.aquarium
37
38 import java.io.File
39
40 import com.ckkloverdos.maybe._
41 import com.ckkloverdos.props.Props
42 import com.ckkloverdos.convert.Converters.{DefaultConverters => TheDefaultConverters}
43
44 import gr.grnet.aquarium.service._
45 import gr.grnet.aquarium.util.{Lifecycle, Loggable, shortNameOfClass}
46 import gr.grnet.aquarium.store._
47 import gr.grnet.aquarium.connector.rabbitmq.service.RabbitMQService
48 import gr.grnet.aquarium.converter.StdConverters
49 import com.ckkloverdos.resource.FileStreamResource
50
51 /**
52  * The master configurator. Responsible to load all of application configuration and provide the relevant services.
53  *
54  * @author Christos KK Loverdos <loverdos@gmail.com>.
55  */
56 final class Configurator(val props: Props) extends Loggable {
57   import Configurator.Keys
58
59   /**
60    * Reflectively provide a new instance of a class and configure it appropriately.
61    */
62   private[this] def newInstance[C : Manifest](_className: String = ""): C = {
63     val className = _className match {
64       case "" ⇒
65         manifest[C].erasure.getName
66
67       case name ⇒
68         name
69     }
70
71     val instanceM = MaybeEither(defaultClassLoader.loadClass(className).newInstance().asInstanceOf[C])
72     instanceM match {
73       case Just(instance) ⇒ instance match {
74         case configurable: Configurable ⇒
75           val localProps = configurable.propertyPrefix match {
76             case Some(prefix) ⇒
77               props.subsetForKeyPrefix(prefix)
78
79             case None ⇒
80               props
81           }
82
83           MaybeEither(configurable configure localProps) match {
84             case Just(_) ⇒
85               logger.debug("Configured {} with props", configurable.getClass.getName)
86               instance
87
88             case Failed(e) ⇒
89               throw new AquariumInternalError("Could not configure instance of %s".format(className), e)
90           }
91
92         case _ ⇒
93           instance
94       }
95
96       case Failed(e) ⇒
97         throw new AquariumInternalError("Could not instantiate %s".format(className), e)
98     }
99
100   }
101
102   private[this] lazy val _actorProvider = newInstance[RoleableActorProviderService](props(Keys.actor_provider_class))
103
104   /**
105    * Initializes a store provider, according to the value configured
106    * in the configuration file. The
107    */
108   private[this] lazy val _storeProvider = newInstance[StoreProvider](props(Keys.store_provider_class))
109   
110   private[this] lazy val _restService = newInstance[Lifecycle](props(Keys.rest_service_class))
111
112   private[this] lazy val _userStateStoreM: Maybe[UserStateStore] = {
113     // If there is a specific `UserStateStore` implementation specified in the
114     // properties, then this implementation overrides the user store given by
115     // `StoreProvider`.
116     props.get(Keys.user_state_store_class) map { className ⇒
117       val instance = newInstance[UserStateStore](className)
118       logger.info("Overriding %s provisioning. Implementation given by: %s".format(
119         shortNameOfClass(classOf[UserStateStore]),
120         instance.getClass))
121       instance
122     }
123   }
124
125   private[this] lazy val _resourceEventStoreM: Maybe[ResourceEventStore] = {
126     // If there is a specific `EventStore` implementation specified in the
127     // properties, then this implementation overrides the event store given by
128     // `StoreProvider`.
129     props.get(Keys.resource_event_store_class) map { className ⇒
130       val instance = newInstance[ResourceEventStore](className)
131       logger.info("Overriding EventStore provisioning. Implementation given by: %s".format(instance.getClass))
132       instance
133     }
134   }
135
136   private[this] lazy val _imEventStoreM: Maybe[IMEventStore] = {
137     props.get(Keys.user_event_store_class) map { className ⇒
138       val instance = newInstance[IMEventStore](className)
139       logger.info("Overriding IMEventStore provisioning. Implementation given by: %s".format(instance.getClass))
140       instance
141     }
142   }
143
144   private[this] lazy val _WalletEventStoreM: Maybe[WalletEntryStore] = {
145     // If there is a specific `IMStore` implementation specified in the
146     // properties, then this implementation overrides the event store given by
147     // `IMProvider`.
148     props.get(Keys.wallet_entry_store_class) map {
149       className ⇒
150         val instance = newInstance[WalletEntryStore](className)
151         logger.info("Overriding WalletEntryStore provisioning. Implementation given by: %s".format(instance.getClass))
152         instance
153     }
154   }
155
156   private[this] lazy val _policyStoreM: Maybe[PolicyStore] = {
157     props.get(Keys.policy_store_class) map {
158       className ⇒
159         val instance = newInstance[PolicyStore](className)
160         logger.info("Overriding PolicyStore provisioning. Implementation given by: %s".format(instance.getClass))
161         instance
162     }
163   }
164
165   private[this] lazy val _eventsStoreFolder: Maybe[File] = {
166     props.get(Keys.events_store_folder) map {
167       folderName ⇒
168         val canonicalFolder = {
169           val folder = new File(folderName)
170           if(folder.isAbsolute) {
171             folder.getCanonicalFile
172           } else {
173             logger.info("{} is not absolute, making it relative to AQUARIUM_HOME", Keys.events_store_folder)
174             new File(ResourceLocator.Homes.Folders.AquariumHome, folderName).getCanonicalFile
175           }
176         }
177
178         val canonicalPath = canonicalFolder.getCanonicalPath
179
180         logger.info("{} = {}", Keys.events_store_folder, canonicalPath)
181
182         if(canonicalFolder.exists() && !canonicalFolder.isDirectory) {
183           throw new AquariumInternalError("%s = %s is not a folder".format(Keys.events_store_folder, canonicalFolder))
184         }
185
186         // Now, events folder must be outside AQUARIUM_HOME, since AQUARIUM_HOME can be wiped out for an upgrade but
187         // we still want to keep the events.
188         val ahCanonicalPath = ResourceLocator.Homes.Folders.AquariumHome.getCanonicalPath
189         if(canonicalPath.startsWith(ahCanonicalPath)) {
190           throw new AquariumException(
191             "%s = %s is under Aquarium Home = %s".format(
192               Keys.events_store_folder,
193               canonicalFolder,
194               ahCanonicalPath
195             ))
196         }
197
198         canonicalFolder.mkdirs()
199
200         canonicalFolder
201     }
202   }
203
204   private[this] lazy val _converters = StdConverters.AllConverters
205
206   private[this] lazy val _timerService: TimerService = newInstance[SimpleTimerService]()
207
208   private[this] lazy val _akka = newInstance[AkkaService]()
209
210   private[this] lazy val _eventBus = newInstance[EventBusService]()
211
212   private[this] lazy val _rabbitmqService = newInstance[RabbitMQService]()
213
214   private[this] lazy val _allServices = List(
215     _timerService,
216     _akka,
217     _actorProvider,
218     _eventBus,
219     _restService,
220     _rabbitmqService
221   )
222
223   def get(key: String, default: String = ""): String = props.getOr(key, default)
224
225   def defaultClassLoader = Thread.currentThread().getContextClassLoader
226
227   /**
228    * FIXME: This must be ditched.
229    * 
230    * Find a file whose location can be overiden in
231    * the configuration file (e.g. policy.yaml)
232    *
233    * @param name Name of the file to search for
234    * @param prop Name of the property that defines the file path
235    * @param default Name to return if no file is found
236    */
237   def findConfigFile(name: String, prop: String, default: String): File = {
238     // Check for the configured value first
239     val configured = new File(get(prop))
240     if (configured.exists)
241       return configured
242
243     // Look into the configuration context
244     ResourceLocator.getResource(name) match {
245       case Just(policyResource) ⇒
246         val path = policyResource.url.getPath
247         new File(path)
248       case _ ⇒
249         new File(default)
250     }
251   }
252
253   def startServices(): Unit = {
254     for(service ← _allServices) {
255       service.start()
256     }
257   }
258
259   def stopServices(): Unit = {
260     _allServices.reverse.foreach(service ⇒ safeUnit(service.stop()))
261   }
262
263   def stopServicesWithDelay(millis: Long) {
264     Thread sleep millis
265     stopServices()
266   }
267
268   def converters = _converters
269   
270   def actorProvider = _actorProvider
271
272   def eventBus = _eventBus
273
274   def timerService = _timerService
275
276   def userStateStore = {
277     _userStateStoreM match {
278       case Just(us) ⇒ us
279       case _        ⇒ storeProvider.userStateStore
280     }
281   }
282
283   def resourceEventStore = {
284     _resourceEventStoreM match {
285       case Just(es) ⇒ es
286       case _        ⇒ storeProvider.resourceEventStore
287     }
288   }
289
290   def walletStore = {
291     _WalletEventStoreM match {
292       case Just(es) ⇒ es
293       case _        ⇒ storeProvider.walletEntryStore
294     }
295   }
296
297   def imEventStore = {
298     _imEventStoreM match {
299       case Just(es) ⇒ es
300       case _        ⇒ storeProvider.imEventStore
301     }
302   }
303
304   def policyStore = {
305     _policyStoreM match {
306       case Just(es) ⇒ es
307       case _        ⇒ storeProvider.policyStore
308     }
309   }
310
311   def storeProvider = _storeProvider
312   
313   def withStoreProviderClass[C <: StoreProvider](spc: Class[C]): Configurator = {
314     val map = this.props.map
315     val newMap = map.updated(Keys.store_provider_class, spc.getName)
316     val newProps = new Props(newMap)
317     new Configurator(newProps)
318   }
319
320   def eventsStoreFolder = _eventsStoreFolder
321
322   def adminCookie: MaybeOption[String] = props.get(Configurator.Keys.admin_cookie) match {
323     case just @ Just(_) ⇒ just
324     case _ ⇒ NoVal
325   }
326 }
327
328 object Configurator {
329   implicit val DefaultConverters = TheDefaultConverters
330
331   final val PolicyConfName = ResourceLocator.ResourceNames.POLICY_YAML
332
333   final val RolesAgreementsName = ResourceLocator.ResourceNames.ROLE_AGREEMENTS_MAP
334
335   final lazy val AquariumPropertiesResource = ResourceLocator.Resources.AquariumPropertiesResource
336
337   final lazy val AquariumProperties = {
338     val maybeProps = Props(AquariumPropertiesResource)
339     maybeProps match {
340       case Just(props) ⇒
341         props
342
343       case NoVal ⇒
344         throw new AquariumInternalError(
345           "Could not load %s from %s".format(
346             ResourceLocator.ResourceNames.AQUARIUM_PROPERTIES,
347             AquariumPropertiesResource))
348
349
350       case Failed(e) ⇒
351         throw new AquariumInternalError(
352           "Could not load %s from %s".format(
353             ResourceLocator.ResourceNames.AQUARIUM_PROPERTIES,
354             AquariumPropertiesResource),
355           e)
356     }
357   }
358
359   lazy val MasterConfigurator = {
360     Maybe(new Configurator(AquariumProperties)) match {
361       case Just(masterConf) ⇒
362         masterConf
363
364       case NoVal ⇒
365         throw new AquariumInternalError(
366           "Could not create Aquarium configuration from %s".format(
367             AquariumPropertiesResource))
368
369       case Failed(e) ⇒
370         throw new AquariumInternalError(
371           "Could not create Aquarium configuration from %s".format(
372             AquariumPropertiesResource),
373           e)
374     }
375   }
376
377   /**
378    * Defines the names of all the known keys inside the master properties file.
379    */
380   final object Keys {
381
382     /**
383      * The Aquarium version. Will be reported in any due occasion.
384      */
385     final val version = "version"
386
387     /**
388      * The fully qualified name of the class that implements the `RoleableActorProviderService`.
389      * Will be instantiated reflectively and should have a public default constructor.
390      */
391     final val actor_provider_class = "actor.provider.class"
392
393     /**
394      * The class that initializes the REST service
395      */
396     final val rest_service_class = "rest.service.class"
397
398     /**
399      * The fully qualified name of the class that implements the `StoreProvider`.
400      * Will be instantiated reflectively and should have a public default constructor.
401      */
402     final val store_provider_class = "store.provider.class"
403
404     /**
405      * The class that implements the User store
406      */
407     final val user_state_store_class = "user.state.store.class"
408
409     /**
410      * The class that implements the resource event store
411      */
412     final val resource_event_store_class = "resource.event.store.class"
413
414     /**
415      * The class that implements the IM event store
416      */
417     final val user_event_store_class = "user.event.store.class"
418
419     /**
420      * The class that implements the wallet entries store
421      */
422     final val wallet_entry_store_class = "wallet.entry.store.class"
423
424     /**
425      * The class that implements the wallet entries store
426      */
427     final val policy_store_class = "policy.store.class"
428
429
430     /** The lower mark for the UserActors' LRU.
431      *
432      * The terminology is borrowed from the (also borrowed) Apache-lucene-solr-based implementation.
433      *
434      */
435     final val user_actors_lru_lower_mark = "user.actors.LRU.lower.mark"
436
437     /**
438      * The upper mark for the UserActors' LRU.
439      *
440      * The terminology is borrowed from the (also borrowed) Apache-lucene-solr-based implementation.
441      */
442     final val user_actors_lru_upper_mark = "user.actors.LRU.upper.mark"
443
444     /**
445      * REST service listening port.
446      *
447      * Default is 8080.
448      */
449     final val rest_port = "rest.port"
450
451     /**
452      * Location of the Aquarium accounting policy config file
453      */
454     final val aquarium_policy = "aquarium.policy"
455
456     /**
457      * Location of the role-agreement mapping file
458      */
459     final val aquarium_role_agreement_map = "aquarium.role-agreement.map"
460     
461     /**
462      * A time period in milliseconds for which we can tolerate stale data regarding user state.
463      *
464      * The smaller the value, the more accurate the user credits and other state data are.
465      *
466      * If a request for user state (e.g. balance) is received and the request timestamp exceeds
467      * the timestamp of the last known balance amount by this value, then a re-computation for
468      * the balance is triggered.
469      */
470     final val user_state_timestamp_threshold = "user.state.timestamp.threshold"
471
472     /**
473      * The time unit is the lowest billable time period.
474      * For example, with a time unit of ten seconds, if a VM is started up and shut down in nine
475      * seconds, then the user will be billed for ten seconds.
476      *
477      * This is an overall constant. We use it as a property in order to prepare ourselves for
478      * multi-cloud setup, where the same Aquarium instance is used to bill several distinct cloud
479      * infrastructures.
480      */
481     final val time_unit_in_millis = "time.unit.in.seconds"
482
483     /**
484      * If a value is given to this property, then it represents a folder where all events coming to aquarium are
485      * stored.
486      */
487     final val events_store_folder = "events.store.folder"
488
489     /**
490      * If set to `true`, then an IM event that cannot be parsed to [[gr.grnet.aquarium.event.model.im.IMEventModel]] is
491      * saved to the [[gr.grnet.aquarium.store.IMEventStore]].
492      */
493     final val save_unparsed_event_im = "save.unparsed.event.im"
494
495     /**
496      * A cookie used in every administrative REST API call, so that Aquarium knows it comes from
497      * an authorised client.
498      */
499     final val admin_cookie = "admin.cookie"
500   }
501
502   object HTTP {
503     final val RESTAdminHeaderName = "X-Aquarium-Admin-Cookie"
504     final val RESTAdminHeaderNameLowerCase = RESTAdminHeaderName.toLowerCase
505   }
506 }