root/src/Tycoon/Bid.py

Revision 1537:8d6508ffee6b, 20.2 kB (checked in by klai@…, 9 months ago)

Clean up multi-cpu code and add error checking for auctioneer

  • Property exe set to *
Line 
1#!/usr/bin/python --
2#
3# Copyright (C) 2005 Kevin Lai
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18#
19import datetime, re, time, xmlrpclib
20from decimal import Decimal, InvalidOperation
21import KL.Measurement.SI
22from Virtualization.Resources import Resources
23from KL.Utility.Command import ArgumentError
24
25class Preference(object):
26   
27    __pref_re = None
28   
29    def __init__(
30        self, 
31        # Relative minimum bids for the resources
32        weight=Decimal(1),
33        # Minimum resource share
34        min_resource=None,
35        # Maximum resource share
36        max_resource=None,
37        # Ratio of total funds to be used to meet resource minimums
38        contingency_ratio=Decimal(0),
39        # Resource type-specific data
40        data=[],
41        # Minimum resource amount
42        min_resource_amount=None,
43        # Maximum resource amount
44        max_resource_amount=None,
45        #
46        ):
47        """Create a Preference.
48        """
49        self.__dict__.update(locals())
50        del self.self
51        if type(self.weight) == int or type(self.weight) == long:
52            self.weight = Decimal(self.weight)
53        self.__set_dict()
54        # Compute the non-contingency ratio using the fractional part
55        # of the CR. This is a hack to allow fractional non-contingency
56        # ratioes without disrupting the rest of the code.
57        if (self.contingency_ratio == int(self.contingency_ratio) and 
58            self.contingency_ratio > 0):
59            self.non_contingency_ratio = Decimal(0)
60        else:
61            self.non_contingency_ratio = 1 - (
62                self.contingency_ratio - int(self.contingency_ratio))
63
64    def parse(cls, pref_string, config_names):
65        """Parse a preference from a string.
66        """
67        if not cls.__pref_re:
68            cls.__tcp_pref_re = re.compile(
69                "(?P<name>\w+)(,(?P<id>((\d)+)))?:"
70                "(?P<weight>\d+\.?\d*)(,(?P<cr>\d+\.?\d*))?:(?P<data>\d+)$")
71            cls.__ip_pref_re = re.compile(
72                "(?P<name>\w+)(,(?P<id>((\d)+)|(\w+.\w+.\w+.\w+)))?:"
73                "(?P<weight>\d+\.?\d*)(,(?P<cr>\d+\.?\d*))?$")
74            cls.__pref_re = re.compile(
75                "(?P<name>\w+)(,(?P<id>(\d)+))?:"
76                "(?P<weight>\d+\.?\d*)(,(?P<min>(\d+\.?\d*[a-zA-Z]*)))?"
77                "(,(?P<max>(\d+\.?\d*[a-zA-Z]*)))?(,(?P<cr>\d+\.?\d*))?"
78                "(:(?P<data>\d+))?$")
79            cls.__config_re = re.compile("(?P<name>\w+):(?P<value>.+)")
80
81        if pref_string.startswith("TCPv4Port"):
82            m = cls.__tcp_pref_re.match(pref_string)
83        elif pref_string.startswith("IPv4Address"):
84            m = cls.__ip_pref_re.match(pref_string)
85        else:
86            m = (cls.__pref_re.match(pref_string) or
87                 cls.__config_re.match(pref_string))
88        if m:
89            g = m.groupdict()
90        else:
91            raise ArgumentError(
92                "Could not parse bid preference: %s" % (pref_string,))
93        pd = {
94            'weight': None, 'min_resource': None,
95            'min_resource_amount': None, 'max_resource': None,
96            'max_resource_amount': None,
97            }
98        names = (('min', 'min_resource', 'min_resource_amount'),
99                 ('max', 'max_resource', 'max_resource_amount'))
100        if g.get('id'):
101            id = "%s,%s" % (g['name'], g['id'])
102        else:
103            id = g['name']
104        if g.get('weight'):
105            pd['weight'] = Decimal(g['weight'])
106        elif not g['name'] in config_names:
107            raise ArgumentError(
108                "Could not parse bid preference: %s" % (pref_string,))
109
110        one = Decimal(1)
111        cr = Decimal("0.9")
112        if g['name'] == "TCPv4Port":
113            pd.update({'min_resource': one, 'max_resource': one,
114                       'contingency_ratio': cr, 'data':[long(g['data'])]})
115        elif g['name'] == "IPv4Address":
116            pd.update({'min_resource': one, 'max_resource': one,
117                       'contingency_ratio': cr})
118        elif g['name'] in Resources:
119            for name, s_name, a_name in names:
120                psi = g[name]
121                if not psi:
122                    continue
123                parsed_unit = False
124                try:
125                    quantity, base_unit = KL.Measurement.SI.parse(psi)
126                    pd[a_name] = long(quantity)
127                    parsed_unit = True
128                except ValueError:
129                    pass
130                if parsed_unit:
131                    if Resources[g['name']].units != base_unit:
132                        raise ArgumentError(
133                            "Units should be %s instead of %s" % (
134                            Resources[g['name']].units, base_unit))
135                else:
136                    pd[s_name] = psi
137            if g['data']:
138                pd['data'] = [int(g['data'])]
139        elif g['name'] in config_names:
140            return g['name'], g['value']
141        else:
142            raise ArgumentError(
143                "Preference type '%s' is unknown" % (g['name'],))
144           
145        # Temporarily set the CR until we figure out how to
146        # expose it.
147        if not g.get('cr') and not 'contingency_ratio' in pd:
148            if g['name'] == 'disk':
149                pd['contingency_ratio'] = Decimal(1)
150            elif g['name'] == 'memory':
151                pd['contingency_ratio'] = Decimal("0.9")
152            else:
153                pd['contingency_ratio'] = Decimal(0)
154        elif g['cr']:
155            pd['contingency_ratio'] = Decimal(g['cr'])
156
157        pref = Preference(**pd)
158        try:
159            #pref.verify(True)
160            pass
161        except (TypeError, ValueError), e:
162            raise ArgumentError(e.args[0])
163        return id, pref
164
165    parse = classmethod(parse)
166   
167    def __check_var(self, name, var, minimum, maximum, var_type, accept_none):
168        t = type(var)
169        if var != None:
170            if not t in var_type:
171                raise TypeError("%s:%s must be a %s instead of a %s" % (
172                    name, var, var_type, t))
173            if var < minimum or (maximum != None and var > maximum):
174                raise ValueError("%s must be in [%s,%s] instead of %s" % (
175                    name, minimum, maximum, var))
176        elif not accept_none:
177            raise ValueError("%s cannot be None" % (name,))
178       
179    def verify(self, accept_none):
180        """Verify that a preference is fully formed.
181        """
182        if (not accept_none or self.weight != None):
183            if ((type(self.weight) != Decimal) or self.weight < 0):
184                raise TypeError(
185                    "Weight must be a non-negative decimal instead of %s" % (
186                    self.weight,))
187        if (not accept_none or self.weight != None):
188            if type(self.data) != list and type(self.data) != tuple:
189                raise TypeError(
190                    "Data must be a list or tuple instead of %s" % (
191                    type(self.data)))
192        self.__check_var(
193            "contingency ratio", self.contingency_ratio, 0, None,
194            (Decimal,), accept_none)
195        self.__check_var(
196            "minimum resource", self.min_resource, 0, 1, (Decimal, ), True)
197        self.__check_var(
198            "maximum resource", self.max_resource, 0, 1, (Decimal,), True)
199        self.__check_var(
200            "minimum resource amount", self.min_resource_amount, 0, None,
201            (long, int), True)
202        self.__check_var(
203            "maximum resource amount", self.max_resource_amount, 0, None,
204            (long, int), True)
205        if ((self.min_resource != None and self.max_resource != None) and
206            (self.min_resource > self.max_resource)):
207            raise ValueError(
208                "Maximum resource share %s must exceed the minimum %s" % (
209                self.max_resource, self.min_resource))
210
211        if not ((self.min_resource != None) ^
212                (self.min_resource_amount != None)):
213            raise ValueError(
214                "Must specify a minimum share or amount instead of %s and %s" % (
215                self.min_resource, self.min_resource_amount))
216        if not ((self.max_resource != None) ^
217                (self.max_resource_amount != None)):
218            raise ValueError(
219                "Must specify a maximum share or amount instead of %s and %s" % (
220                self.max_resource, self.max_resource_amount))
221       
222        if ((self.min_resource_amount != None and
223             self.max_resource_amount != None) and
224            (self.min_resource_amount > self.max_resource_amount)):
225            raise ValueError(
226                "Maximum resource amount %d must exceed the minimum %d" % (
227                self.max_resource_amount, self.min_resource_amount))
228
229    def normalize(self, capacity):
230        """
231        """
232        min_resource = self.min_resource
233        if min_resource == None:
234            min_resource = min(
235                self.min_resource_amount / capacity, Decimal(1))
236        max_resource = self.max_resource
237        if max_resource == None:
238            max_resource = min(
239                self.max_resource_amount / capacity, Decimal(1))
240        return min_resource, max_resource
241
242    def __getstate__(self):
243        return (
244            self.weight, self.min_resource, self.max_resource,
245            self.contingency_ratio, self.data, self.min_resource_amount,
246            self.max_resource_amount)
247   
248    def __setstate__(self, t):
249        self.__init__(*t)
250           
251    def __set_dict(self):
252        """Get a non-object representation.
253        """
254    def __repr__(self):
255        return "%s" % (self.get_dict(),)
256   
257    def get_dict(self):
258        d = {
259            'weight': self.weight, 'min_resource': self.min_resource,
260            'max_resource': self.max_resource, 'data': self.data, 
261            'contingency_ratio': self.contingency_ratio,
262            'min_resource_amount': self.min_resource_amount,
263            'max_resource_amount': self.max_resource_amount
264            }
265        return d
266   
267    def update(self, new_pref):
268        """
269        Note that preferences are verified after they are updated, so
270        a preference here has not been verified.
271        """
272        changed = False
273        if new_pref.weight != self.weight:
274            changed = True
275        self.weight = new_pref.weight
276        if new_pref.min_resource != None:
277            if new_pref.min_resource != self.min_resource:
278                changed = True
279            self.min_resource = new_pref.min_resource
280            self.min_resource_amount = None
281        elif new_pref.min_resource_amount != None:
282            if new_pref.min_resource_amount != self.min_resource_amount:
283                changed = True
284            self.min_resource_amount = new_pref.min_resource_amount
285            self.min_resource = None
286        if new_pref.max_resource != None:
287            if new_pref.max_resource != self.max_resource:
288                changed = True
289            self.max_resource = new_pref.max_resource
290            self.max_resource_amount = None
291        elif new_pref.max_resource_amount != None:
292            if new_pref.max_resource_amount != self.max_resource_amount:
293                changed = True
294            self.max_resource_amount = new_pref.max_resource_amount
295            self.max_resource = None
296        # Changed data does not trigger changes in the bid
297        if new_pref.data != None:
298            self.data = new_pref.data
299        if new_pref.contingency_ratio != None:
300            if new_pref.contingency_ratio != self.contingency_ratio:
301                changed = True
302            self.contingency_ratio = new_pref.contingency_ratio
303        self.__set_dict()
304
305        return changed
306
307    def __convert(value):
308        if value == None:
309            return value
310        elif type(value) == float:
311            return Decimal(str(value))
312        else:
313            try:
314                d = Decimal(value)
315            except:
316                d = value
317            return d
318
319    __convert = staticmethod(__convert)
320       
321    def create(pref_dict):
322        d = pref_dict
323        c = Preference.__convert
324        return Preference(
325            d['weight'], c(d['min_resource']), c(d['max_resource']),
326            c(d['contingency_ratio']), d['data'], d['min_resource_amount'],
327            d['max_resource_amount'])
328    create = staticmethod(create)
329
330class Bid(object):
331
332    __duration_re = None
333   
334    def __init__(
335        self, duration=0L, preferences={}, expiration=0L):
336        """
337        If duration == 0 and expiration == 0, then the existing duration and
338        expiration are used.
339        If duration != 0 and expiration == 0, then a new expiration is
340        calculated on the auctioneer.
341        If expiration != 0, then the expiration is used in the auctioneer's
342        time space.
343        """
344        self.__dict__.update(locals())
345        del self.self
346        # Check that the bid is well-formed: has correct types and ranges
347        if (type(self.duration) != long or self.duration < 0):
348            raise TypeError(
349                "Duration must be a long >= 0 instead of %s" % (
350                self.duration,))
351        if (type(self.expiration) != long or self.expiration < 0):
352            raise TypeError(
353                "Expiration must be a long >= 0 instead of %s" % (
354                self.expiration,))
355        self.sum_weights()
356
357    def __add_month_year(date, month, year):
358        # Subtract one to shift from 1..12 to 0..11 space
359        months = year * 12.0 + month - 1 + date.month
360
361        # Whole years
362        years, leftover_months = divmod(months, 12.0)
363        long_years = date.year + long(years)
364       
365        # Whole months, add one to shift from 0..11 to 1..12 space
366        long_months = long(leftover_months) + 1
367
368        fractional_months = leftover_months - long(leftover_months)
369        return (date.replace(year=long_years, month=long_months),
370                fractional_months)
371    __add_month_year = staticmethod(__add_month_year)
372   
373    def __parse_duration(s):
374        """Parse one or two durations from a string.
375        """
376        if not Bid.__duration_re:
377            Bid.__duration_re = re.compile(
378                "^((?P<s>[0-9.]+)([sS]|$))?((?P<m>[0-9.]+)[mM])?"
379                "((?P<h>[0-9.]+)[hH])?((?P<d>[0-9.]+)[dD])?"
380                "((?P<w>[0-9.]+)[wW])?((?P<o>[0-9.]+)[oO])?"
381                "((?P<y>[0-9.]+)[yY])?$")
382        return [Bid.__parse_one_duration(sp) for sp in s.split(",")]
383    __parse_duration = staticmethod(__parse_duration)
384           
385    def __parse_one_duration(s):
386        """Parse one duration from a string.
387        """
388        m = Bid.__duration_re.search(s)
389        if m:
390            g = m.groupdict(0)
391        else:
392            raise ArgumentError("Could not parse bid duration: %s" % (s,))
393        # Some time units can be directly converted
394        duration = long(
395            float(g['s']) + 60 * float(g['m']) + 3600 * float(g['h']) +
396            86400 * float(g['d']) + 604800 * float(g['w']))
397        now = datetime.datetime.now()
398        # The library does not support year and month conversion,
399        # probably because there is no absolute definition of how
400        # long a year and month are. Leap years are longer than non-leap
401        # years, and months can have several different lengths. This
402        # code assumes that all month and year durations are relative
403        # to the current time. Unfortunately, this means that the
404        # same string definition will result in different in numerical
405        # amounts at different times, which can be confusing.
406       
407        # Account for all year amounts and month amounts greater than or
408        # equal to a year.
409        # Convert into months
410        then, fractional_months = Bid.__add_month_year(
411            now, float(g['o']), float(g['y']))
412
413        # Handle fractional months
414        offset = 0
415        if fractional_months > 0.0:
416            next_month, _ = Bid.__add_month_year(then, 1, 0)
417            after = time.mktime(next_month.timetuple())
418            secs = after - time.mktime(then.timetuple())
419            offset = secs * fractional_months
420
421        duration = duration + long(
422            time.mktime(then.timetuple()) - time.mktime(now.timetuple()) +
423            offset)
424
425        return duration
426    __parse_one_duration = staticmethod(__parse_one_duration)
427   
428    def parse(
429        args, duration=(0L,), preferences=None, config=None, config_names=()):
430        """Parse a bid from strings.
431        """
432        if len(args) >= 1:
433            d = Bid.__parse_duration(args[0])
434            if d != 0:
435                duration = d
436
437        if not preferences:
438            preferences = {}
439           
440        if not config:
441            config = {}
442           
443        if len(args) >= 2:
444            new_prefs = {}
445            for ps in args[1:]:
446                k, v = Preference.parse(ps, config_names)
447                if k in config_names:
448                    config[k] = v
449                else:
450                    new_prefs[k] = v
451           
452            preferences.update(new_prefs)
453           
454        bid = Bid(duration[0], preferences)
455        d2 = None
456        if len(duration) > 1:
457            d2 = duration[1]
458        return bid, config, d2
459
460    parse = staticmethod(parse)
461
462    def create(bid_dict):
463        d = bid_dict
464        prefs = dict([(k, Preference.create(v))
465                      for k,v in d['preferences'].iteritems()])
466        return Bid(d['duration'], prefs, d['expiration'])
467    create = staticmethod(create)
468   
469    def __getstate__(self):
470        return (
471            self.duration, self.preferences, self.expiration,)
472       
473    def __setstate__(self, t):
474        self.__init__(*t)
475
476    def sum_weights(self):
477        values = self.preferences.values()
478        self.total_weight = sum([p.weight for p in values])
479        self.total_contingency_weight = sum(
480            [pref.weight * (1 - pref.non_contingency_ratio) for pref in values])
481       
482    def add_preference(self, key, pref):
483        """
484        """
485        self.preferences[key] = pref
486        self.sum_weights()
487
488    def update(self, new_bid):
489        """Update the old bid using the new bid.
490        """
491        changed = False
492        old_expiration = self.expiration
493        if new_bid.duration != 0:
494            self.expiration = long(time.time() + new_bid.duration)
495            self.duration = new_bid.duration
496        elif new_bid.expiration != 0:
497            self.expiration = new_bid.expiration
498           
499        if self.expiration != old_expiration:
500            changed = True
501
502        for r_name, r in new_bid.preferences.iteritems():
503            # Copy new preferences
504            if not r_name in self.preferences:
505                self.preferences[r_name] = r
506                changed = True
507            else:
508                changed |= self.preferences[r_name].update(r)
509               
510        self.sum_weights()
511        return changed
512       
513    def normalize_time(self):
514        if self.duration != 0 and self.expiration == 0:
515            self.expiration = long(time.time() + self.duration)
516
517    def get_dict(self):
518        """Get a non-object representation.
519        """
520        pref = dict([(k, v.get_dict()) for k,v in self.preferences.iteritems()])
521        return {'duration': self.duration, 'expiration': self.expiration,
522                'preferences': pref}
523       
524def main(argv=None):
525    """Test the code.
526    """
527    cpu = Preference(1, 1000000, 2000000, 0.5)
528    memory = Preference(2, 500000000, 1000000000, 0.75)
529    port = Preference(3, data=2048)
530    b = Bid(100, {("cpu",): cpu, ("memory",): memory, ('TCPv4Port', 22): port})
531    s = b.xml_rpc_serialize()
532    print s
533    new_bid = xml_rpc_parse(s)
534    print new_bid
535    port = Preference(0, data=2048)
536    disk = Preference(4, 500000000, 1000000000, 0.75)
537    b = Bid(0, {("disk",): disk, ('TCPv4Port', 22): port})
538    b.update(new_bid)
539    print b
540
541if __name__ == "__main__":
542    import sys
543    sys.exit(main())
Note: See TracBrowser for help on using the browser.