First cut of the new policy configuration system
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / Policy.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.logic.accounting
37
38 import dsl.{Timeslot, DSLPolicy, DSL}
39 import java.io.{InputStream, FileInputStream, File}
40 import java.util.Date
41 import gr.grnet.aquarium.util.Loggable
42 import java.util.concurrent.atomic.AtomicReference
43 import collection.immutable.{TreeMap, SortedMap}
44 import gr.grnet.aquarium.{ResourceLocator, AquariumAwareSkeleton, AquariumException, Aquarium}
45 import gr.grnet.aquarium.util.date.{MutableDateCalc, TimeHelpers}
46 import com.ckkloverdos.maybe.{Failed, Just}
47
48 /**
49  * Searches for and loads the applicable accounting policy
50  *
51  * @author Georgios Gousios <gousiosg@gmail.com>
52  */
53 object Policy extends DSL with Loggable with AquariumAwareSkeleton {
54
55   /* Pointer to the latest policy */
56   private[logic] lazy val policies = {
57     new AtomicReference[SortedMap[Timeslot, DSLPolicy]](reloadPolicies)
58   }
59
60   /* Pointer to the latest policy */
61   private lazy val currentPolicy = {new AtomicReference[DSLPolicy](latestPolicy)}
62
63   /* Aquarium to use for loading information about the policy store */
64   private var config: Aquarium = _
65
66   /**
67    * Get the latest defined policy.
68    */
69   def policy = currentPolicy.get
70
71   /**
72    * Get the policy that is valid at the specified time instance.
73    */
74   def policy(at: Date): DSLPolicy = {
75     policies.get.find {
76       a => (a._1.from.before(at) && a._1.to.after(at)) ||
77            (a._1.from.before(at) && a._1.to == Long.MaxValue)
78     } match {
79       case Some(x) => x._2
80       case None =>
81         throw new AquariumException("No valid policy for date: %s".format(new MutableDateCalc(at)))
82     }
83   }
84
85   /**
86    * Get the policies that are valid between the specified time instances,
87    * in a map whose keys are sorted by time.
88    */
89   def policies(from: Date, to: Date): SortedMap[Timeslot, DSLPolicy] = {
90     policies.get.filter {
91       a => a._1.from.before(from) &&
92            a._1.to.after(to)
93     }
94   }
95
96   /**
97    * Get the policies that are valid throughout the specified
98    * [[gr.grnet.aquarium.logic.accounting.dsl.Timeslot]]
99    */
100   def policies(t: Timeslot): SortedMap[Timeslot, DSLPolicy] = policies(t.from, t.to)
101
102   /**
103    * Load and parse a policy from file.
104    */
105   def loadPolicyFromFile(pol: File): DSLPolicy = {
106
107     val stream = pol.exists() match {
108       case true =>
109         logger.info("Using policy file %s".format(pol.getAbsolutePath))
110         new FileInputStream(pol)
111       case false =>
112         logger.warn(("Cannot find user configured policy file %s, " +
113           "looking for default policy").format(pol.getAbsolutePath))
114         getClass.getClassLoader.getResourceAsStream("policy.yaml") match {
115           case x: InputStream =>
116             logger.warn("Using default policy, this is problably not what you want")
117             x
118           case null =>
119             logger.error("No valid policy file found, Aquarium will fail")
120             null
121         }
122     }
123     parse(stream)
124   }
125
126   /**
127    * Trigger a policy update cycle.
128    */
129   def updatePolicies = synchronized {
130     //XXX: The following update should happen as one transaction
131     val tmpPol = reloadPolicies
132     currentPolicy.set(latestPolicy)
133     policies.set(tmpPol)
134   }
135
136   /**
137    * Find the latest policy in the list of policies.
138    */
139   private def latestPolicy =
140     policies.get.foldLeft((Timeslot(new Date(1), new Date(2)) -> DSLPolicy.emptyPolicy)) {
141       (acc, p) =>
142         if (acc._2 == DSLPolicy.emptyPolicy)
143           p
144         else if (p._1.after(acc._1))
145           p
146         else
147           acc
148     }._2
149
150   /**
151    * Set the configurator to use for loading policy stores. Should only
152    * used for unit testing.
153    */
154   def withConfigurator(config: Aquarium): Unit =
155     this.config = config
156
157   /**
158    * Check whether the policy definition file (in whichever path) is
159    * newer than the latest stored policy, reload and set it as current.
160    * This method has side-effects to this object's state.
161    */
162   private[logic] def reloadPolicies: SortedMap[Timeslot, DSLPolicy] =
163     if (config == null)
164       reloadPolicies(aquarium)
165     else
166       reloadPolicies(config)
167
168   private def reloadPolicies(config: Aquarium):
169   SortedMap[Timeslot, DSLPolicy] = {
170     //1. Load policies from db
171     val pol = config.policyStore.loadPolicyEntriesAfter(0)
172
173     //2. Check whether policy file has been updated
174     val latestPolicyChange = if (pol.isEmpty) 0 else pol.last.validFrom
175
176     val policyf = ResourceLocator.Resources.PolicyYAMLResource
177     var updated = false
178
179     if (policyf.exists) {
180         updated = true
181      } else {
182       logger.warn("User specified policy file %s does not exist, " +
183         "using stored policy information".format(policyf.url))
184     }
185
186     if (updated) {
187       val ts = TimeHelpers.nowMillis()
188       val parsedNewM = policyf.mapInputStream(parse).toMaybeEither
189       val parsedNew = parsedNewM match {
190         case Just(parsedNew) ⇒ parsedNew
191         case Failed(e)       ⇒ throw e
192       }
193       val newPolicy = parsedNew.toPolicyEntry
194
195       config.policyStore.findPolicyEntry(newPolicy.id) match {
196         case Some(x) =>
197           logger.warn("Policy file contents not modified")
198         case None =>
199           if (!pol.isEmpty) {
200             val toUpdate = pol.last.copy(validTo = ts - 1)
201             config.policyStore.updatePolicyEntry(toUpdate)
202             config.policyStore.storePolicyEntry(newPolicy)
203           } else {
204             config.policyStore.storePolicyEntry(newPolicy)
205           }
206       }
207     }
208
209     config.policyStore.loadPolicyEntriesAfter(0).foldLeft(new TreeMap[Timeslot, DSLPolicy]()){
210       (acc, p) =>
211         acc ++ Map(Timeslot(p.validFrom, p.validTo) -> parse(p.policyYAML))
212     }
213   }
214
215   Policy.policy
216 }