Project

General

Profile

root / branches / 1.1 / src / haizea / core / leases.py @ 798

1
# -------------------------------------------------------------------------- #
2
# Copyright 2006-2009, University of Chicago                                 #
3
# Copyright 2008-2009, Distributed Systems Architecture Group, Universidad   #
4
# Complutense de Madrid (dsa-research.org)                                   #
5
#                                                                            #
6
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
7
# not use this file except in compliance with the License. You may obtain    #
8
# a copy of the License at                                                   #
9
#                                                                            #
10
# http://www.apache.org/licenses/LICENSE-2.0                                 #
11
#                                                                            #
12
# Unless required by applicable law or agreed to in writing, software        #
13
# distributed under the License is distributed on an "AS IS" BASIS,          #
14
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
15
# See the License for the specific language governing permissions and        #
16
# limitations under the License.                                             #
17
# -------------------------------------------------------------------------- #
18

    
19
"""This module provides the lease data structures:
20

21
* Lease: Represents a lease
22
* LeaseStateMachine: A state machine to keep track of a lease's state
23
* Capacity: Used to represent a quantity of resources
24
* Timestamp: An exact moment in time
25
* Duration: A duration
26
* SoftwareEnvironment, UnmanagedSoftwareEnvironment, DiskImageSoftwareEnvironment:
27
  Used to represent a lease's required software environment.
28
* LeaseWorkload: Represents a collection of lease requests submitted
29
  in a specific order.
30
* Site: Represents the site with leasable resources.
31
* Nodes: Represents a collection of machines ("nodes"). This is used
32
  both when specifying a site and when specifying the machines
33
  needed by a leases.
34
"""
35

    
36
from haizea.common.constants import LOGLEVEL_VDEBUG, RES_MEM, SUSPRES_EXCLUSION_GLOBAL, SUSPRES_EXCLUSION_LOCAL
37
from haizea.common.utils import StateMachine, round_datetime_delta, get_lease_id, compute_suspend_resume_time, get_config
38
from haizea.core.scheduler.slottable import ResourceReservation
39

    
40
from mx.DateTime import DateTime, TimeDelta, Parser
41

    
42
import logging
43

    
44
try:
45
    import xml.etree.ElementTree as ET
46
except ImportError:
47
    # Compatibility with Python <=2.4
48
    import elementtree.ElementTree as ET 
49

    
50

    
51

    
52

    
53
class Lease(object):
54
    """A resource lease
55
    
56
    This is one of the main data structures used in Haizea. A lease
57
    is "a negotiated and renegotiable agreement between a resource 
58
    provider and a resource consumer, where the former agrees to make 
59
    a set of resources available to the latter, based on a set of 
60
    lease terms presented by the resource consumer". All the gory
61
    details on what this means can be found on the Haizea website
62
    and on the Haizea publications.
63
    
64
    See the __init__ method for a description of the information that
65
    is contained in a lease.
66
    
67
    """
68
    
69
    # Lease states
70
    STATE_NEW = 0
71
    STATE_PENDING = 1
72
    STATE_REJECTED = 2
73
    STATE_SCHEDULED = 3
74
    STATE_QUEUED = 4
75
    STATE_CANCELLED = 5
76
    STATE_PREPARING = 6
77
    STATE_READY = 7
78
    STATE_ACTIVE = 8
79
    STATE_SUSPENDING = 9
80
    STATE_SUSPENDED_PENDING = 10
81
    STATE_SUSPENDED_QUEUED = 11
82
    STATE_SUSPENDED_SCHEDULED = 12
83
    STATE_MIGRATING = 13
84
    STATE_RESUMING = 14
85
    STATE_RESUMED_READY = 15
86
    STATE_DONE = 16
87
    STATE_FAIL = 17
88
    STATE_REJECTED_BY_USER = 18
89
    
90
    # String representation of lease states
91
    state_str = {STATE_NEW : "New",
92
                 STATE_PENDING : "Pending",
93
                 STATE_REJECTED : "Rejected",
94
                 STATE_SCHEDULED : "Scheduled",
95
                 STATE_QUEUED : "Queued",
96
                 STATE_CANCELLED : "Cancelled",
97
                 STATE_PREPARING : "Preparing",
98
                 STATE_READY : "Ready",
99
                 STATE_ACTIVE : "Active",
100
                 STATE_SUSPENDING : "Suspending",
101
                 STATE_SUSPENDED_PENDING : "Suspended-Pending",
102
                 STATE_SUSPENDED_QUEUED : "Suspended-Queued",
103
                 STATE_SUSPENDED_SCHEDULED : "Suspended-Scheduled",
104
                 STATE_MIGRATING : "Migrating",
105
                 STATE_RESUMING : "Resuming",
106
                 STATE_RESUMED_READY: "Resumed-Ready",
107
                 STATE_DONE : "Done",
108
                 STATE_FAIL : "Fail",
109
                 STATE_REJECTED_BY_USER : "Rejected by user"}
110
    
111
    # Lease types
112
    BEST_EFFORT = 1
113
    ADVANCE_RESERVATION = 2
114
    IMMEDIATE = 3
115
    DEADLINE = 4
116
    UNKNOWN = -1
117
    
118
    # String representation of lease types    
119
    type_str = {BEST_EFFORT: "Best-effort",
120
                ADVANCE_RESERVATION: "AR",
121
                IMMEDIATE: "Immediate",
122
                DEADLINE: "Deadline",
123
                UNKNOWN: "Unknown"}
124
    
125
    def __init__(self, lease_id, submit_time, user_id, requested_resources, start, duration, 
126
                 deadline, preemptible, software, state, extras = {}):
127
        """Constructs a lease.
128
        
129
        The arguments are the fundamental attributes of a lease.
130
        The attributes that are not specified by the arguments are
131
        the lease ID (which is an autoincremented integer), the
132
        lease state (a lease always starts out in state "NEW").
133
        A lease also has several bookkeeping attributes that are
134
        only meant to be consumed by other Haizea objects.
135
        
136
        Arguments:
137
        id -- Unique identifier for the lease. If None, one
138
        will be provided.
139
        submit_time -- The time at which the lease was submitted
140
        requested_resources -- A dictionary (int -> Capacity) mapping
141
          each requested node to a capacity (i.e., the amount of
142
          resources requested for that node)
143
        start -- A Timestamp object containing the requested time.
144
        duration -- A Duration object containing the requested duration.
145
        deadline -- A Timestamp object containing the deadline by which
146
          this lease must be completed.
147
        preemptible -- A boolean indicating whether this lease can be
148
          preempted or not.
149
        software -- A SoftwareEnvironment object specifying the
150
          software environment required by the lease.
151
        extras -- Extra attributes. Haizea will ignore them, but they
152
          may be used by pluggable modules.
153
        """        
154
        # Lease ID (read only)
155
        self.id = lease_id
156
        
157
        # Lease attributes
158
        self.submit_time = submit_time
159
        self.user_id = user_id
160
        self.requested_resources = requested_resources
161
        self.start = start
162
        self.duration = duration
163
        self.deadline = deadline
164
        self.preemptible = preemptible
165
        self.software = software
166
        self.price = None
167
        self.extras = extras
168

    
169
        # Bookkeeping attributes:
170

    
171
        # Lease state
172
        if state == None:
173
            state = Lease.STATE_NEW
174
        self.state_machine = LeaseStateMachine(initial_state = state)
175

    
176
        # End of lease (recorded when the lease ends)
177
        self.end = None
178
        
179
        # Number of nodes requested in the lease
180
        self.numnodes = len(requested_resources)
181
        
182
        # The following two lists contain all the resource reservations
183
        # (or RRs) associated to this lease. These two lists are
184
        # basically the link between the lease and Haizea's slot table.
185
        
186
        # The preparation RRs are reservations that have to be
187
        # completed before a lease can first transition into a
188
        # READY state (e.g., image transfers)
189
        self.preparation_rrs = []
190
        # The VM RRs are reservations for the VMs that implement
191
        # the lease.
192
        self.vm_rrs = []
193

    
194
        # Enactment information. Should only be manipulated by enactment module
195
        self.enactment_info = None
196
        self.vnode_enactment_info = dict([(n, None) for n in self.requested_resources.keys()])
197
        
198
        
199
    @classmethod
200
    def create_new(cls, submit_time, user_id, requested_resources, start, duration, 
201
                 deadline, preemptible, software):
202
        lease_id = get_lease_id()
203
        state = Lease.STATE_NEW
204
        return cls(lease_id, submit_time, user_id, requested_resources, start, duration, 
205
                 deadline, preemptible, software, state)
206
        
207
    @classmethod
208
    def create_new_from_xml_element(cls, element):
209
        lease = cls.from_xml_element(element)
210
        if lease.id == None:
211
            lease.id = get_lease_id()
212
        lease.state_machine = LeaseStateMachine(initial_state = Lease.STATE_NEW)
213
        return lease
214

    
215
    @classmethod
216
    def from_xml_file(cls, xml_file):
217
        """Constructs a lease from an XML file.
218
        
219
        See the Haizea documentation for details on the
220
        lease XML format.
221
        
222
        Argument:
223
        xml_file -- XML file containing the lease in XML format.
224
        """        
225
        return cls.from_xml_element(ET.parse(xml_file).getroot())
226

    
227
    @classmethod
228
    def from_xml_string(cls, xml_str):
229
        """Constructs a lease from an XML string.
230
        
231
        See the Haizea documentation for details on the
232
        lease XML format.
233
        
234
        Argument:
235
        xml_str -- String containing the lease in XML format.
236
        """        
237
        return cls.from_xml_element(ET.fromstring(xml_str))
238
        
239
    @classmethod
240
    def from_xml_element(cls, element):
241
        """Constructs a lease from an ElementTree element.
242
        
243
        See the Haizea documentation for details on the
244
        lease XML format.
245
        
246
        Argument:
247
        element -- Element object containing a "<lease>" element.
248
        """        
249
        
250
        lease_id = element.get("id")
251
        
252
        if lease_id == None:
253
            lease_id = None
254
        else:
255
            lease_id = int(lease_id)
256

    
257
        user_id = element.get("user")
258
        if user_id == None:
259
            user_id = None
260
        else:
261
            user_id = int(user_id)
262

    
263
        state = element.get("state")
264
        if state == None:
265
            state = None
266
        else:
267
            state = int(state)
268

    
269
        
270
        submit_time = element.get("submit-time")
271
        if submit_time == None:
272
            submit_time = None
273
        else:
274
            submit_time = Parser.DateTimeFromString(submit_time)
275
        
276
        nodes = Nodes.from_xml_element(element.find("nodes"))
277
        
278
        requested_resources = nodes.get_all_nodes()
279
        
280
        start = element.find("start")
281
        if len(start.getchildren()) == 0:
282
            start = Timestamp(Timestamp.UNSPECIFIED)
283
        else:
284
            child = start[0]
285
            if child.tag == "now":
286
                start = Timestamp(Timestamp.NOW)
287
            elif child.tag == "exact":
288
                start = Timestamp(Parser.DateTimeFromString(child.get("time")))
289
        
290
        duration = Duration(Parser.DateTimeDeltaFromString(element.find("duration").get("time")))
291

    
292
        deadline = element.find("deadline")
293
        
294
        if deadline != None:
295
            deadline = Parser.DateTimeFromString(deadline.get("time"))
296
        
297
        extra = element.find("extra")
298
        extras = {}
299
        if extra != None:
300
            for attr in extra:
301
                extras[attr.get("name")] = attr.get("value")
302

    
303
        
304
        preemptible = element.get("preemptible").capitalize()
305
        if preemptible == "True":
306
            preemptible = True
307
        elif preemptible == "False":
308
            preemptible = False
309
        
310
        software = element.find("software")
311
        
312
        if software.find("none") != None:
313
            software = UnmanagedSoftwareEnvironment()
314
        elif software.find("disk-image") != None:
315
            disk_image = software.find("disk-image")
316
            image_id = disk_image.get("id")
317
            image_size = int(disk_image.get("size"))
318
            software = DiskImageSoftwareEnvironment(image_id, image_size)
319
        
320
        return Lease(lease_id, submit_time, user_id, requested_resources, start, duration, 
321
                     deadline, preemptible, software, state, extras)
322

    
323

    
324
    def to_xml(self):
325
        """Returns an ElementTree XML representation of the lease
326
        
327
        See the Haizea documentation for details on the
328
        lease XML format.
329
        
330
        """        
331
        lease = ET.Element("lease")
332
        if self.id != None:
333
            lease.set("id", str(self.id))
334
        lease.set("state", str(self.get_state()))
335
        lease.set("preemptible", str(self.preemptible))
336
        if self.submit_time != None:
337
            lease.set("submit-time", str(self.submit_time))
338
        
339
        capacities = {}
340
        for capacity in self.requested_resources.values():
341
            key = capacity
342
            for c in capacities:
343
                if capacity == c:
344
                    key = c
345
                    break
346
            numnodes = capacities.setdefault(key, 0)
347
            capacities[key] += 1
348
        
349
        nodes = Nodes([(numnodes,c) for c,numnodes in capacities.items()])
350
        lease.append(nodes.to_xml())
351
        
352
        start = ET.SubElement(lease, "start")
353
        if self.start.requested == Timestamp.UNSPECIFIED:
354
            pass # empty start element
355
        elif self.start.requested == Timestamp.NOW:
356
            ET.SubElement(start, "now") #empty now element
357
        else:
358
            exact = ET.SubElement(start, "exact")
359
            exact.set("time", str(self.start.requested))
360
            
361
        duration = ET.SubElement(lease, "duration")
362
        duration.set("time", str(self.duration.requested))
363
        
364
        software = ET.SubElement(lease, "software")
365
        if isinstance(self.software, UnmanagedSoftwareEnvironment):
366
            ET.SubElement(software, "none")
367
        elif isinstance(self.software, DiskImageSoftwareEnvironment):
368
            imagetransfer = ET.SubElement(software, "disk-image")
369
            imagetransfer.set("id", self.software.image_id)
370
            imagetransfer.set("size", str(self.software.image_size))
371
            
372
        return lease
373

    
374
    def to_xml_string(self):
375
        """Returns a string XML representation of the lease
376
        
377
        See the Haizea documentation for details on the
378
        lease XML format.
379
        
380
        """   
381
        return ET.tostring(self.to_xml())
382

    
383
    def get_type(self):
384
        """Determines the type of lease
385
        
386
        Based on the lease's attributes, determines the lease's type.
387
        Can return Lease.BEST_EFFORT, Lease.ADVANCE_RESERVATION, or
388
        Lease.IMMEDIATE
389
        
390
        """
391
        if self.start.requested == Timestamp.UNSPECIFIED:
392
            return Lease.BEST_EFFORT
393
        elif self.start.requested == Timestamp.NOW:
394
            return Lease.IMMEDIATE            
395
        else:
396
            if self.deadline == None:
397
                return Lease.ADVANCE_RESERVATION
398
            else:
399
                return Lease.DEADLINE
400
        
401
    def get_state(self):
402
        """Returns the lease's state.
403
                
404
        """        
405
        return self.state_machine.get_state()
406
    
407
    def set_state(self, state):
408
        """Changes the lease's state.
409
                
410
        The state machine will throw an exception if the 
411
        requested transition is illegal.
412
        
413
        Argument:
414
        state -- The new state
415
        """        
416
        self.state_machine.change_state(state)
417
        
418
    def print_contents(self, loglevel=LOGLEVEL_VDEBUG):
419
        """Prints the lease's attributes to the log.
420
                
421
        Argument:
422
        loglevel -- The loglevel at which to print the information
423
        """           
424
        logger = logging.getLogger("LEASES")
425
        logger.log(loglevel, "__________________________________________________")
426
        logger.log(loglevel, "Lease ID       : %i" % self.id)
427
        logger.log(loglevel, "Type           : %s" % Lease.type_str[self.get_type()])
428
        logger.log(loglevel, "Submission time: %s" % self.submit_time)
429
        logger.log(loglevel, "Start          : %s" % self.start)
430
        logger.log(loglevel, "Duration       : %s" % self.duration)
431
        logger.log(loglevel, "Deadline       : %s" % self.deadline)
432
        logger.log(loglevel, "State          : %s" % Lease.state_str[self.get_state()])
433
        logger.log(loglevel, "Resource req   : %s" % self.requested_resources)
434
        logger.log(loglevel, "Software       : %s" % self.software)
435
        logger.log(loglevel, "Price          : %s" % self.price)
436
        logger.log(loglevel, "Extras         : %s" % self.extras)
437
        self.print_rrs(loglevel)
438
        logger.log(loglevel, "--------------------------------------------------")
439

    
440
    def print_rrs(self, loglevel=LOGLEVEL_VDEBUG):
441
        """Prints the lease's resource reservations to the log.
442
                
443
        Argument:
444
        loglevel -- The loglevel at which to print the information
445
        """            
446
        logger = logging.getLogger("LEASES")  
447
        if len(self.preparation_rrs) > 0:
448
            logger.log(loglevel, "DEPLOYMENT RESOURCE RESERVATIONS")
449
            logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
450
            for r in self.preparation_rrs:
451
                r.print_contents(loglevel)
452
                logger.log(loglevel, "##")
453
        logger.log(loglevel, "VM RESOURCE RESERVATIONS")
454
        logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~")
455
        for r in self.vm_rrs:
456
            r.print_contents(loglevel)
457
            logger.log(loglevel, "##")
458

    
459
    def get_active_vmrrs(self, time):
460
        """Returns the active VM resource reservations at a given time
461
                
462
        Argument:
463
        time -- Time to look for active reservations
464
        """        
465
        return [r for r in self.vm_rrs if r.start <= time and time <= r.end and r.state == ResourceReservation.STATE_ACTIVE]
466

    
467
    def get_scheduled_reservations(self):
468
        """Returns all scheduled reservations
469
                
470
        """           
471
        return [r for r in self.preparation_rrs + self.vm_rrs if r.state == ResourceReservation.STATE_SCHEDULED]
472

    
473
    def get_last_vmrr(self):
474
        """Returns the last VM reservation for this lease.
475
                        
476
        """            
477
        if len(self.vm_rrs) > 0:
478
            return self.vm_rrs[-1]
479
        else:
480
            return None
481
    
482
    def get_vmrr_at(self, time):
483
        """...
484
                        
485
        """
486
        vmrr_at = None
487
        for vmrr in self.vm_rrs:
488
            if time >= vmrr.get_first_start() and time < vmrr.get_final_end():
489
                vmrr_at = vmrr
490
                break
491
        return vmrr_at
492
    
493
    def get_vmrr_after(self, time):
494
        """...
495
                        
496
        """
497
        vmrr_after = []
498
        for vmrr in self.vm_rrs:
499
            if vmrr.get_first_start() > time:
500
                vmrr_after.append(vmrr)
501
        return vmrr_after    
502

    
503
    def get_endtime(self):
504
        """Returns the time at which the last VM reservation 
505
        for this lease ends.
506
        
507
        Note that this is not necessarily the time at which the lease
508
        will end, just the time at which the last currently scheduled
509
        VM will end.
510
                
511
        """        
512
        vmrr = self.get_last_vmrr()
513
        return vmrr.end
514
    
515
    def get_accumulated_duration_at(self, time):
516
        """Returns the amount of time required to fulfil the entire
517
        requested duration of the lease at a given time.
518
                        
519
        """
520
        t = TimeDelta(0)
521
        for vmrr in self.vm_rrs:
522
            if time >= vmrr.end:
523
                t += vmrr.end - vmrr.start
524
            elif time >= vmrr.start and time < vmrr.end:
525
                t += time -vmrr.start
526
                break
527
            else:
528
                break
529
        return t
530

    
531
    
532
    def get_remaining_duration_at(self, time):
533
        """Returns the amount of time required to fulfil the entire
534
        requested duration of the lease at a given time.
535
                        
536
        """
537
        return self.duration.requested - self.get_accumulated_duration_at(time)    
538
    
539
    def append_vmrr(self, vmrr):
540
        """Adds a VM resource reservation to the lease.
541
        
542
        Argument:
543
        vmrr -- The VM RR to add.
544
        """             
545
        self.vm_rrs.append(vmrr)
546
        self._update_prematureend()
547
        
548
    def remove_vmrr(self, vmrr):
549
        """Removes a VM resource reservation from the lease.
550
        
551
        Argument:
552
        vmrr -- The VM RR to remove.
553
        """           
554
        if not vmrr in self.vm_rrs:
555
            raise Exception, "Tried to remove an VM RR not contained in this lease"
556
        else:
557
            self.vm_rrs.remove(vmrr)
558
                    
559
    def append_preparationrr(self, preparation_rr):
560
        """Adds a preparation resource reservation to the lease.
561
        
562
        Argument:
563
        preparation_rr -- The preparation RR to add.
564
        """             
565
        self.preparation_rrs.append(preparation_rr)
566
        
567
    def remove_preparationrr(self, preparation_rr):
568
        """Removes a preparation resource reservation from the lease.
569
        
570
        Argument:
571
        preparation_rr -- The preparation RR to remove.
572
        """        
573
        if not preparation_rr in self.preparation_rrs:
574
            raise Exception, "Tried to remove a preparation RR not contained in this lease"
575
        else:
576
            self.preparation_rrs.remove(preparation_rr)        
577

    
578
    def clear_rrs(self):
579
        """Removes all resource reservations for this lease
580
        (both preparation and VM)
581
        
582
        """            
583
        self.preparation_rrs = []
584

    
585
        for rr in self.vm_rrs: 
586
            rr.clear_rrs()
587
        self.vm_rrs = []
588

    
589
    def get_waiting_time(self):
590
        """Gets the waiting time for this lease.
591
        
592
        The waiting time is the difference between the submission
593
        time and the time at which the lease start. This method
594
        mostly makes sense for best-effort leases, where the
595
        starting time is determined by Haizea.
596
        
597
        """          
598
        return self.start.actual - self.submit_time
599
        
600
    def get_slowdown(self, bound=10):
601
        """Determines the bounded slowdown for this lease.
602
        
603
        Slowdown is a normalized measure of how much time a
604
        request takes to make it through a queue (thus, like
605
        get_waiting_time, the slowdown makes sense mostly for
606
        best-effort leases). Slowdown is equal to the time the
607
        lease took to run on a loaded system (i.e., a system where
608
        it had to compete with other leases for resources)
609
        divided by the time it would take if it just had the
610
        system all to itself (i.e., starts running immediately
611
        without having to wait in a queue and without the
612
        possibility of being preempted).
613
        
614
        "Bounded" slowdown is one where leases with very short
615
        durations are rounded up to a bound, to prevent the
616
        metric to be affected by reasonable but disproportionate
617
        waiting times (e.g., a 5-second lease with a 15 second
618
        waiting time -an arguably reasonable waiting time- has a 
619
        slowdown of 4, the same as 10 hour lease having to wait 
620
        30 hours for resources).
621
        
622
        Argument:
623
        bound -- The bound, specified in seconds.
624
        All leases with a duration less than this
625
        parameter are rounded up to the bound.
626
        """          
627
        time_on_dedicated = self.duration.original
628
        time_on_loaded = self.end - self.submit_time
629
        bound = TimeDelta(seconds=bound)
630
        if time_on_dedicated < bound:
631
            time_on_dedicated = bound
632
        return time_on_loaded / time_on_dedicated
633
 
634
    
635
    def estimate_suspend_time(self):
636
        """ Estimate the time to suspend an entire lease
637
                            
638
        Most of the work is done in __estimate_suspend_resume_time. See
639
        that method's documentation for more details.
640
        
641
        Arguments:
642
        lease -- Lease that is going to be suspended
643
        
644
        """               
645
        rate = get_config().get("suspend-rate")
646
        override = get_config().get("override-suspend-time")
647
        if override != None:
648
            return override
649
        else:
650
            return self.__estimate_suspend_resume_time(rate)
651

    
652

    
653
    def estimate_resume_time(self):
654
        """ Estimate the time to resume an entire lease
655
                            
656
        Most of the work is done in __estimate_suspend_resume_time. See
657
        that method's documentation for more details.
658
        
659
        Arguments:
660
        lease -- Lease that is going to be resumed
661
        
662
        """           
663
        rate = get_config().get("resume-rate") 
664
        override = get_config().get("override-resume-time")
665
        if override != None:
666
            return override
667
        else:
668
            return self.__estimate_suspend_resume_time(rate)    
669

    
670
    def estimate_shutdown_time(self):
671
        """ Estimate the time to shutdown an entire lease
672
                            
673
        Arguments:
674
        lease -- Lease that is going to be shutdown
675
        
676
        """            
677
        enactment_overhead = get_config().get("enactment-overhead").seconds
678
        return get_config().get("shutdown-time") + (enactment_overhead * self.numnodes)
679

    
680
    def add_boot_overhead(self, t):
681
        """Adds a boot overhead to the lease.
682
        
683
        Increments the requested duration to account for the fact 
684
        that some time will be spent booting up the resources.
685
        
686
        Argument:
687
        t -- Time to add
688
        """          
689
        self.duration.incr(t)   
690

    
691
    def add_runtime_overhead(self, percent):
692
        """Adds a runtime overhead to the lease.
693
        
694
        This method is mostly meant for simulations. Since VMs
695
        run slower than physical hardware, this increments the
696
        duration of a lease by a percent to observe the effect
697
        of having all the leases run slower on account of
698
        running on a VM.
699
        
700
        Note: the whole "runtime overhead" problem is becoming
701
        increasingly moot as people have lost their aversion to
702
        VMs thanks to the cloud computing craze. Anecdotal evidence
703
        suggests that most people don't care that VMs will run
704
        X % slower (compared to a physical machine) because they
705
        know full well that what they're getting is a virtual
706
        machine (the same way a user of an HPC system would know
707
        that he/she's getting processors with speed X as opposed to
708
        those on some other site, with speed X*0.10)
709
        
710
        Argument:
711
        percent -- Runtime overhead (in percent of requested
712
        duration) to add to the lease.
713
        """            
714
        self.duration.incr_by_percent(percent)
715
            
716
    def sanity_check(self):
717
        prev_time = None
718
        prev_vmrr = None
719
        for vmrr in self.vm_rrs:
720
            if len(vmrr.pre_rrs) > 0:
721
                prev_time = vmrr.pre_rrs[0].start - 1
722
            else:
723
                prev_time = vmrr.start - 1
724
                
725
            if prev_vmrr != None:
726
                if vmrr.is_resuming():
727
                    assert prev_vmrr.is_suspending()
728
            else:
729
                assert not vmrr.is_resuming()
730
                
731
            for pre_rr in vmrr.pre_rrs:
732
                assert pre_rr.start >= prev_time
733
                assert pre_rr.end >= pre_rr.start
734
                prev_time = pre_rr.end
735
                
736
            assert vmrr.start >= prev_time
737
            assert vmrr.end >= vmrr.start
738
            prev_time = vmrr.end
739
            
740
            if vmrr.prematureend != None:
741
                assert vmrr.prematureend >= vmrr.start and vmrr.prematureend <= vmrr.end
742

    
743
            for post_rr in vmrr.post_rrs:
744
                assert post_rr.start >= prev_time
745
                assert post_rr.end >= post_rr.start
746
                prev_time = post_rr.end
747
                
748
            prev_vmrr = vmrr
749
            
750
            
751
    def __estimate_suspend_resume_time(self, rate):
752
        """ Estimate the time to suspend/resume an entire lease
753
                            
754
        Note that, unlike __compute_suspend_resume_time, this estimates
755
        the time to suspend/resume an entire lease (which may involve
756
        suspending several VMs)
757
        
758
        Arguments:
759
        lease -- Lease that is going to be suspended/resumed
760
        rate -- The rate at which an individual VM is suspended/resumed
761
        
762
        """              
763
        susp_exclusion = get_config().get("suspendresume-exclusion")        
764
        enactment_overhead = get_config().get("enactment-overhead") 
765
        mem = 0
766
        for vnode in self.requested_resources:
767
            mem += self.requested_resources[vnode].get_quantity(RES_MEM)
768
        if susp_exclusion == SUSPRES_EXCLUSION_GLOBAL:
769
            return self.numnodes * enactment_overhead + compute_suspend_resume_time(mem, rate)
770
        elif susp_exclusion == SUSPRES_EXCLUSION_LOCAL:
771
            # Overestimating
772
            return self.numnodes * enactment_overhead + compute_suspend_resume_time(mem, rate)
773
        
774
    # ONLY for simulation
775
    def _update_prematureend(self):
776
        known = self.duration.known
777
        acc = TimeDelta(0)
778
        for vmrr in self.vm_rrs:
779
            if known != None:
780
                rrdur = vmrr.end - vmrr.start
781
                if known - acc <= rrdur:
782
                    vmrr.prematureend = vmrr.start + (known-acc)
783
                    break                 
784
                else:
785
                    vmrr.prematureend = None
786
                    acc += rrdur
787
            else:
788
                vmrr.prematureend = None
789
        
790
        
791
class LeaseStateMachine(StateMachine):
792
    """A lease state machine
793
    
794
    A child of StateMachine, this class simply specifies the valid
795
    states and transitions for a lease (the actual state machine code
796
    is in StateMachine).
797
    
798
    See the Haizea documentation for a description of states and
799
    valid transitions.
800
    
801
    """
802
    transitions = {Lease.STATE_NEW:                 [(Lease.STATE_PENDING,    "")],
803
                   
804
                   Lease.STATE_PENDING:             [(Lease.STATE_SCHEDULED,  ""),
805
                                                     (Lease.STATE_QUEUED,     ""),
806
                                                     (Lease.STATE_CANCELLED,  ""),
807
                                                     (Lease.STATE_REJECTED,   ""),
808
                                                     (Lease.STATE_REJECTED_BY_USER,   "")],
809
                                                     
810
                   Lease.STATE_SCHEDULED:           [(Lease.STATE_PREPARING,  ""),
811
                                                     (Lease.STATE_QUEUED,     ""),
812
                                                     (Lease.STATE_PENDING,    ""),
813
                                                     (Lease.STATE_READY,      ""),
814
                                                     (Lease.STATE_CANCELLED,  ""),
815
                                                     (Lease.STATE_FAIL,       "")],
816
                                                     
817
                   Lease.STATE_QUEUED:              [(Lease.STATE_SCHEDULED,  ""),
818
                                                     (Lease.STATE_CANCELLED,  "")],
819
                                                     
820
                   Lease.STATE_PREPARING:           [(Lease.STATE_READY,      ""),
821
                                                     (Lease.STATE_PENDING,     ""),
822
                                                     (Lease.STATE_CANCELLED,  ""),
823
                                                     (Lease.STATE_FAIL,       "")],
824
                                                     
825
                   Lease.STATE_READY:               [(Lease.STATE_ACTIVE,     ""),
826
                                                     (Lease.STATE_QUEUED,     ""),
827
                                                     (Lease.STATE_PENDING,     ""),
828
                                                     (Lease.STATE_CANCELLED,  ""),
829
                                                     (Lease.STATE_FAIL,       "")],
830
                                                     
831
                   Lease.STATE_ACTIVE:              [(Lease.STATE_SUSPENDING, ""),
832
                                                     (Lease.STATE_READY,     ""),
833
                                                     (Lease.STATE_QUEUED,     ""),
834
                                                     (Lease.STATE_DONE,       ""),
835
                                                     (Lease.STATE_CANCELLED,  ""),
836
                                                     (Lease.STATE_FAIL,       "")],
837
                                                     
838
                   Lease.STATE_SUSPENDING:          [(Lease.STATE_SUSPENDED_PENDING,  ""),
839
                                                     (Lease.STATE_CANCELLED,  ""),
840
                                                     (Lease.STATE_FAIL,       "")],
841
                                                     
842
                   Lease.STATE_SUSPENDED_PENDING:   [(Lease.STATE_SUSPENDED_QUEUED,     ""),
843
                                                     (Lease.STATE_SUSPENDED_SCHEDULED,  ""),
844
                                                     (Lease.STATE_CANCELLED,  ""),
845
                                                     (Lease.STATE_FAIL,       "")],
846
                                                     
847
                   Lease.STATE_SUSPENDED_QUEUED:    [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
848
                                                     (Lease.STATE_CANCELLED,  ""),
849
                                                     (Lease.STATE_FAIL,       "")],
850
                                                     
851
                   Lease.STATE_SUSPENDED_SCHEDULED: [(Lease.STATE_SUSPENDED_QUEUED,     ""),
852
                                                     (Lease.STATE_SUSPENDED_PENDING,  ""),
853
                                                     (Lease.STATE_MIGRATING,  ""),
854
                                                     (Lease.STATE_RESUMING,   ""),
855
                                                     (Lease.STATE_CANCELLED,  ""),
856
                                                     (Lease.STATE_FAIL,       "")],
857
                                                     
858
                   Lease.STATE_MIGRATING:           [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
859
                                                     (Lease.STATE_CANCELLED,  ""),
860
                                                     (Lease.STATE_FAIL,       "")],
861
                                                     
862
                   Lease.STATE_RESUMING:            [(Lease.STATE_RESUMED_READY, ""),
863
                                                     (Lease.STATE_CANCELLED,  ""),
864
                                                     (Lease.STATE_FAIL,       "")],
865
                                                     
866
                   Lease.STATE_RESUMED_READY:       [(Lease.STATE_ACTIVE,     ""),
867
                                                     (Lease.STATE_CANCELLED,  ""),
868
                                                     (Lease.STATE_FAIL,       "")],
869
                   
870
                   # Final states
871
                   Lease.STATE_DONE:          [],
872
                   Lease.STATE_CANCELLED:     [],
873
                   Lease.STATE_FAIL:          [],
874
                   Lease.STATE_REJECTED:      [],
875
                   }
876
    
877
    def __init__(self, initial_state):
878
        StateMachine.__init__(self, initial_state, LeaseStateMachine.transitions, Lease.state_str)
879

    
880

    
881
class Capacity(object):
882
    """A quantity of resources
883
    
884
    This class is used to represent a quantity of resources, such
885
    as those required by a lease. For example, if a lease needs a
886
    single node with 1 CPU and 1024 MB of memory, a single Capacity
887
    object would be used containing that information. 
888
    
889
    Resources in a Capacity object can be multi-instance, meaning
890
    that several instances of the same type of resources can be
891
    specified. For example, if a node requires 2 CPUs, then this is
892
    represented as two instances of the same type of resource. Most
893
    resources, however, will be "single instance" (e.g., a physical
894
    node only has "one" memory).
895
    
896
    Note: This class is similar, but distinct from, the ResourceTuple
897
    class in the slottable module. The ResourceTuple class can contain
898
    the same information, but uses a different internal representation
899
    (which is optimized for long-running simulations) and is tightly
900
    coupled to the SlotTable class. The Capacity and ResourceTuple
901
    classes are kept separate so that the slottable module remains
902
    independent from the rest of Haizea (in case we want to switch
903
    to a different slottable implementation in the future).
904
    
905
    """        
906
    def __init__(self, types):
907
        """Constructs an empty Capacity object.
908
        
909
        All resource types are initially set to be single-instance,
910
        with a quantity of 0 for each resource.
911
        
912
        Argument:
913
        types -- List of resource types. e.g., ["CPU", "Memory"]
914
        """          
915
        self.ninstances = dict([(res_type, 1) for res_type in types])
916
        self.quantity = dict([(res_type, [0]) for res_type in types])
917
        
918
    def get_ninstances(self, res_type):
919
        """Gets the number of instances for a resource type
920
                
921
        Argument:
922
        type -- The type of resource (using the same name passed
923
        when constructing the Capacity object)
924
        """               
925
        return self.ninstances[res_type]
926
           
927
    def get_quantity(self, res_type):
928
        """Gets the quantity of a single-instance resource
929
                
930
        Argument:
931
        type -- The type of resource (using the same name passed
932
        when constructing the Capacity object)
933
        """               
934
        return self.get_quantity_instance(res_type, 1)
935
    
936
    def get_quantity_instance(self, res_type, instance):
937
        """Gets the quantity of a specific instance of a 
938
        multi-instance resource.
939
                        
940
        Argument:
941
        type -- The type of resource (using the same name passed
942
        when constructing the Capacity object)
943
        instance -- The instance. Note that instances are numbered
944
        from 1.
945
        """               
946
        return self.quantity[res_type][instance-1]
947

    
948
    def set_quantity(self, res_type, amount):
949
        """Sets the quantity of a single-instance resource
950
                
951
        Argument:
952
        type -- The type of resource (using the same name passed
953
        when constructing the Capacity object)
954
        amount -- The amount to set the resource to.
955
        """            
956
        self.set_quantity_instance(res_type, 1, amount)
957
    
958
    def set_quantity_instance(self, res_type, instance, amount):
959
        """Sets the quantity of a specific instance of a 
960
        multi-instance resource.
961
                        
962
        Argument:
963
        type -- The type of resource (using the same name passed
964
        when constructing the Capacity object)
965
        instance -- The instance. Note that instances are numbered
966
        from 1.
967
        amount -- The amount to set the instance of the resource to.
968
        """        
969
        self.quantity[res_type][instance-1] = amount
970
    
971
    def set_ninstances(self, res_type, ninstances):
972
        """Changes the number of instances of a resource type.
973
                        
974
        Note that changing the number of instances will initialize
975
        all the instances' amounts to zero. This method should
976
        only be called right after constructing a Capacity object.
977
        
978
        Argument:
979
        type -- The type of resource (using the same name passed
980
        when constructing the Capacity object)
981
        ninstance -- The number of instances
982
        """                
983
        self.ninstances[res_type] = ninstances
984
        self.quantity[res_type] = [0] * ninstances
985
       
986
    def get_resource_types(self):
987
        """Returns the types of resources in this capacity.
988
                        
989
        """            
990
        return self.quantity.keys()
991
    
992
    @classmethod
993
    def from_resources_string(cls, resource_str):
994
        """Constructs a site from a "resources string"
995
        
996
        A "resources string" is a shorthand way of specifying a capacity:
997
        
998
        <resource_type>:<resource_quantity>[,<resource_type>:<resource_quantity>]*
999
        
1000
        For example: CPU:100,Memory:1024
1001
        
1002
        Argument:
1003
        resource_str -- resources string
1004
        """    
1005
        res = {}
1006
        resources = resource_str.split(",")
1007
        for r in resources:
1008
            res_type, amount = r.split(":")
1009
            res[res_type] = int(amount)
1010
            
1011
        capacity = cls(res.keys())
1012
        for (res_type, amount) in res.items():
1013
            capacity.set_quantity(res_type, amount)
1014

    
1015
        return capacity
1016
    
1017
    def __eq__(self, other):
1018
        """Tests if two capacities are the same
1019
                        
1020
        """        
1021
        for res_type in self.quantity:
1022
            if not other.quantity.has_key(res_type):
1023
                return False
1024
            if self.ninstances[res_type] != other.ninstances[res_type]:
1025
                return False
1026
            if self.quantity[res_type] != other.quantity[res_type]:
1027
                return False
1028
        return True
1029

    
1030
    def __ne__(self, other):
1031
        """Tests if two capacities are not the same
1032
                        
1033
        """        
1034
        return not self == other
1035
            
1036
    def __repr__(self):
1037
        """Returns a string representation of the Capacity"""
1038
        return "  |  ".join("%s: %i" % (res_type,q[0]) for res_type, q in self.quantity.items())
1039
            
1040

    
1041
class Timestamp(object):
1042
    """An exact point in time.
1043
    
1044
    This class is just a wrapper around three DateTimes. When
1045
    dealing with timestamps in Haizea (such as the requested
1046
    starting time for a lease), we want to keep track not just
1047
    of the requested timestamp, but also the scheduled timestamp
1048
    (which could differ from the requested one) and the
1049
    actual timestamp (which could differ from the scheduled one).
1050
    """        
1051
    
1052
    UNSPECIFIED = "Unspecified"
1053
    NOW = "Now"
1054
    
1055
    def __init__(self, requested):
1056
        """Constructor
1057
                        
1058
        Argument:
1059
        requested -- The requested timestamp
1060
        """        
1061
        self.requested = requested
1062
        self.scheduled = None
1063
        self.actual = None
1064

    
1065
    def __repr__(self):
1066
        """Returns a string representation of the Duration"""
1067
        return "REQ: %s  |  SCH: %s  |  ACT: %s" % (self.requested, self.scheduled, self.actual)
1068
    
1069
    def is_requested_exact(self):
1070
        return self.requested != Timestamp.UNSPECIFIED and self.requested != Timestamp.NOW
1071
        
1072
class Duration(object):
1073
    """A duration
1074
    
1075
    This class is just a wrapper around five DateTimes. When
1076
    dealing with durations in Haizea (such as the requested
1077
    duration for a lease), we want to keep track of the following:
1078
    
1079
    - The requested duration
1080
    - The accumulated duration (when the entire duration of
1081
    the lease can't be scheduled without interrumption, this
1082
    keeps track of how much duration has been fulfilled so far)
1083
    - The actual duration (which might not be the same as the
1084
    requested duration)
1085
    
1086
    For the purposes of simulation, we also want to keep track
1087
    of the "original" duration (since the requested duration
1088
    can be modified to simulate certain overheads) and the
1089
    "known" duration (when simulating lease workloads, this is
1090
    the actual duration of the lease, which is known a posteriori).
1091
    """  
1092
    
1093
    def __init__(self, requested, known=None):
1094
        """Constructor
1095
                        
1096
        Argument:
1097
        requested -- The requested duration
1098
        known -- The known duration (ONLY in simulation)
1099
        """              
1100
        self.original = requested
1101
        self.requested = requested
1102
        self.accumulated = TimeDelta()
1103
        self.actual = None
1104
        # The following is ONLY used in simulation
1105
        self.known = known
1106
        
1107
    def incr(self, t):
1108
        """Increments the requested duration by an amount.
1109
                        
1110
        Argument:
1111
        t -- The time to add to the requested duration.
1112
        """               
1113
        self.requested += t
1114
        if self.known != None:
1115
            self.known += t
1116
            
1117
    def incr_by_percent(self, pct):
1118
        """Increments the requested duration by a percentage.
1119
                        
1120
        Argument:
1121
        pct -- The percentage of the requested duration to add.
1122
        """          
1123
        factor = 1 + float(pct)/100
1124
        self.requested = round_datetime_delta(self.requested * factor)
1125
        if self.known != None:
1126
            self.requested = round_datetime_delta(self.known * factor)
1127
        
1128
    def accumulate_duration(self, t):
1129
        """Increments the accumulated duration by an amount.
1130
                        
1131
        Argument:
1132
        t -- The time to add to the accumulated duration.
1133
        """        
1134
        self.accumulated += t
1135
            
1136
    def get_remaining_duration(self):
1137
        """Returns the amount of time required to fulfil the entire
1138
        requested duration of the lease.
1139
                        
1140
        """         
1141
        return self.requested - self.accumulated
1142

    
1143
    def get_remaining_known_duration(self):
1144
        """Returns the amount of time required to fulfil the entire
1145
        known duration of the lease.
1146
              
1147
        ONLY for simulations.
1148
        """
1149
        return self.known - self.accumulated
1150
            
1151
    def __repr__(self):
1152
        """Returns a string representation of the Duration"""
1153
        return "REQ: %s  |  ACC: %s  |  ACT: %s  |  KNW: %s" % (self.requested, self.accumulated, self.actual, self.known)
1154
    
1155
class SoftwareEnvironment(object):
1156
    """The base class for a lease's software environment"""
1157
    
1158
    def __init__(self):
1159
        """Constructor.
1160
        
1161
        Does nothing."""
1162
        pass
1163

    
1164
class UnmanagedSoftwareEnvironment(SoftwareEnvironment):
1165
    """Represents an "unmanaged" software environment.
1166
    
1167
    When a lease has an unmanaged software environment,
1168
    Haizea does not need to perform any actions to prepare
1169
    a lease's software environment (it assumes that this
1170
    task is carried out by an external entity, and that
1171
    software environments can be assumed to be ready
1172
    when a lease has to start; e.g., if VM disk images are
1173
    predeployed on all physical nodes)."""
1174
    
1175
    def __init__(self):
1176
        """Constructor.
1177
        
1178
        Does nothing."""        
1179
        SoftwareEnvironment.__init__(self)
1180

    
1181
class DiskImageSoftwareEnvironment(SoftwareEnvironment):
1182
    """Reprents a software environment encapsulated in a disk image.
1183
    
1184
    When a lease's software environment is contained in a disk image,
1185
    this disk image must be deployed to the physical nodes the lease
1186
    is mapped to before the lease can start. This means that the
1187
    preparation for this lease must be handled by a preparation
1188
    scheduler (see documentation in lease_scheduler) capable of
1189
    handling a DiskImageSoftwareEnvironment.
1190
    """
1191
    def __init__(self, image_id, image_size):
1192
        """Constructor.
1193
        
1194
        Arguments:
1195
        image_id -- A unique identifier for the disk image required
1196
        by the lease.
1197
        image_size -- The size, in MB, of the disk image. """         
1198
        self.image_id = image_id
1199
        self.image_size = image_size
1200
        SoftwareEnvironment.__init__(self)
1201

    
1202

    
1203
    
1204
class LeaseWorkload(object):
1205
    """Reprents a sequence of lease requests.
1206
    
1207
    A lease workload is a sequence of lease requests with a specific
1208
    arrival time for each lease. This class is currently only used
1209
    to load LWF (Lease Workload File) files. See the Haizea documentation 
1210
    for details on the LWF format.
1211
    """    
1212
    def __init__(self, leases):
1213
        """Constructor.
1214
        
1215
        Arguments:
1216
        leases -- An ordered list (by arrival time) of leases in the workload
1217
        """                 
1218
        self.leases = leases
1219
        
1220

    
1221
    def get_leases(self):
1222
        """Returns the leases in the workload.
1223
        
1224
        """  
1225
        return self.leases
1226
    
1227
    @classmethod
1228
    def from_xml_file(cls, xml_file, inittime = DateTime(0)):
1229
        """Constructs a lease workload from an XML file.
1230
        
1231
        See the Haizea documentation for details on the
1232
        lease workload XML format.
1233
        
1234
        Argument:
1235
        xml_file -- XML file containing the lease in XML format.
1236
        inittime -- The starting time of the lease workload. All relative
1237
        times in the XML file will be converted to absolute times by
1238
        adding them to inittime. If inittime is not specified, it will
1239
        arbitrarily be 0000/01/01 00:00:00.
1240
        """        
1241
        return cls.__from_xml_element(ET.parse(xml_file).getroot(), inittime)
1242

    
1243
    @classmethod
1244
    def __from_xml_element(cls, element, inittime):
1245
        """Constructs a lease from an ElementTree element.
1246
        
1247
        See the Haizea documentation for details on the
1248
        lease XML format.
1249
        
1250
        Argument:
1251
        element -- Element object containing a "<lease-workload>" element.
1252
        inittime -- The starting time of the lease workload. All relative
1253
        times in the XML file will be converted to absolute times by
1254
        adding them to inittime.  
1255
        """                
1256
        reqs = element.findall("lease-requests/lease-request")
1257
        leases = []
1258
        for r in reqs:
1259
            lease = r.find("lease")
1260
            # Add time lease is submitted
1261
            submittime = inittime + Parser.DateTimeDeltaFromString(r.get("arrival"))
1262
            lease.set("submit-time", str(submittime))
1263
            
1264
            # If an exact starting time is specified, add the init time
1265
            exact = lease.find("start/exact")
1266
            if exact != None:
1267
                start = inittime + Parser.DateTimeDeltaFromString(exact.get("time"))
1268
                exact.set("time", str(start))
1269

    
1270
            # If a deadline is specified, add the init time
1271
            deadline = lease.find("deadline")
1272
            if deadline != None:
1273
                t = inittime + Parser.DateTimeDeltaFromString(deadline.get("time"))
1274
                deadline.set("time", str(t))
1275
                
1276
            lease = Lease.create_new_from_xml_element(lease)
1277
            
1278
            realduration = r.find("realduration")
1279
            if realduration != None:
1280
                realduration = Parser.DateTimeDeltaFromString(realduration.get("time"))
1281
                if realduration < lease.duration.requested:
1282
                    lease.duration.known = realduration
1283

    
1284
            leases.append(lease)
1285
            
1286
        return cls(leases)
1287
    
1288
class LeaseAnnotation(object):
1289
    """Represents a lease annotation.
1290
    
1291
    ...
1292
    """    
1293
    def __init__(self, lease_id, start, deadline, software, extras):
1294
        """Constructor.
1295
        
1296
        Arguments:
1297
        ...
1298
        """                 
1299
        self.lease_id = lease_id
1300
        self.start = start
1301
        self.deadline = deadline
1302
        self.software = software
1303
        self.extras = extras
1304
        
1305
    
1306
    @classmethod
1307
    def from_xml_file(cls, xml_file):
1308
        """...
1309
        
1310
        ...
1311
        
1312
        Argument:
1313
        xml_file -- XML file containing the lease in XML format.
1314
        """        
1315
        return cls.__from_xml_element(ET.parse(xml_file).getroot())
1316

    
1317
    @classmethod
1318
    def from_xml_element(cls, element):
1319
        """...
1320
        
1321
        ...
1322
        
1323
        Argument:
1324
        element -- Element object containing a "<lease-annotation>" element.
1325
        """                
1326
        lease_id = element.get("id")
1327
      
1328
        start = element.find("start")
1329
        if start != None:
1330
            if len(start.getchildren()) == 0:
1331
                start = Timestamp(Timestamp.UNSPECIFIED)
1332
            else:
1333
                child = start[0]
1334
                if child.tag == "now":
1335
                    start = Timestamp(Timestamp.NOW)
1336
                elif child.tag == "exact":
1337
                    start = Timestamp(Parser.DateTimeDeltaFromString(child.get("time")))
1338
        
1339
        deadline = element.find("deadline")
1340
        
1341
        if deadline != None:
1342
            deadline = Parser.DateTimeDeltaFromString(deadline.get("time"))
1343
        
1344
        extra = element.find("extra")
1345
        extras = {}
1346
        if extra != None:
1347
            for attr in extra:
1348
                extras[attr.get("name")] = attr.get("value")
1349

    
1350
        
1351
        software = element.find("software")
1352

    
1353
        if software != None:
1354
            if software.find("none") != None:
1355
                software = UnmanagedSoftwareEnvironment()
1356
            elif software.find("disk-image") != None:
1357
                disk_image = software.find("disk-image")
1358
                image_id = disk_image.get("id")
1359
                image_size = int(disk_image.get("size"))
1360
                software = DiskImageSoftwareEnvironment(image_id, image_size)
1361
        
1362
        return cls(lease_id, start, deadline, software, extras)
1363
 
1364
    
1365
    def to_xml(self):
1366
        """Returns an ElementTree XML representation of the lease annotation
1367
        
1368
        ...
1369
        
1370
        """        
1371
        annotation = ET.Element("lease-annotation")
1372
        annotation.set("id", str(self.lease_id))
1373
        
1374
        start = ET.SubElement(annotation, "start")
1375
        if self.start.requested == Timestamp.UNSPECIFIED:
1376
            pass # empty start element
1377
        elif self.start.requested == Timestamp.NOW:
1378
            ET.SubElement(start, "now") #empty now element
1379
        else:
1380
            exact = ET.SubElement(start, "exact")
1381
            exact.set("time", "+" + str(self.start.requested))
1382
            
1383
        if self.deadline != None:
1384
            deadline = ET.SubElement(annotation, "deadline")
1385
            deadline.set("time", "+" + str(self.deadline))
1386
        
1387
        if self.software != None:
1388
            software = ET.SubElement(annotation, "software")
1389
            if isinstance(self.software, UnmanagedSoftwareEnvironment):
1390
                ET.SubElement(software, "none")
1391
            elif isinstance(self.software, DiskImageSoftwareEnvironment):
1392
                imagetransfer = ET.SubElement(software, "disk-image")
1393
                imagetransfer.set("id", self.software.image_id)
1394
                imagetransfer.set("size", str(self.software.image_size))
1395
            
1396
        if len(self.extras) > 0:
1397
            extras = ET.SubElement(annotation, "extra")
1398
            for name, value in self.extras.items():
1399
                attr = ET.SubElement(extras, "attr")
1400
                attr.set("name", name)
1401
                attr.set("value", value)
1402
            
1403
        return annotation
1404

    
1405
    def to_xml_string(self):
1406
        """Returns a string XML representation of the lease annotation
1407
        
1408
        ...
1409
        
1410
        """   
1411
        return ET.tostring(self.to_xml())    
1412
    
1413
class LeaseAnnotations(object):
1414
    """Represents a sequence of lease annotations.
1415
    
1416
    ...
1417
    """    
1418
    def __init__(self, annotations, attributes):
1419
        """Constructor.
1420
        
1421
        Arguments:
1422
        annotations -- A dictionary of annotations
1423
        """                 
1424
        self.annotations = annotations
1425
        self.attributes = attributes
1426
    
1427
    def apply_to_leases(self, leases):
1428
        """Apply annotations to a workload
1429
        
1430
        """
1431
        for lease in [l for l in leases if self.has_annotation(l.id)]:
1432
            annotation = self.get_annotation(lease.id)
1433
            
1434
            if annotation.start != None:
1435
                if annotation.start.requested in (Timestamp.NOW, Timestamp.UNSPECIFIED):
1436
                    lease.start.requested = annotation.start.requested
1437
                else:
1438
                    lease.start.requested = lease.submit_time + annotation.start.requested
1439

    
1440
            if annotation.deadline != None:
1441
                lease.deadline = lease.submit_time + annotation.deadline
1442

    
1443
            if annotation.software != None:
1444
                lease.software = annotation.software
1445

    
1446
            if annotation.extras != None:
1447
                lease.extras.update(annotation.extras)
1448

    
1449
    def get_annotation(self, lease_id):
1450
        """...
1451
        
1452
        """  
1453
        return self.annotations[lease_id]
1454

    
1455
    def has_annotation(self, lease_id):
1456
        """...
1457
        
1458
        """  
1459
        return self.annotations.has_key(lease_id)
1460
    
1461
    @classmethod
1462
    def from_xml_file(cls, xml_file):
1463
        """...
1464
        
1465
        ...
1466
        
1467
        Argument:
1468
        xml_file -- XML file containing the lease in XML format.
1469
        """        
1470
        return cls.__from_xml_element(ET.parse(xml_file).getroot())
1471

    
1472
    @classmethod
1473
    def __from_xml_element(cls, element):
1474
        """...
1475
        
1476
        ...
1477
        
1478
        Argument:
1479
        element -- Element object containing a "<lease-annotations>" element.
1480
        """                
1481
        annotation_elems = element.findall("lease-annotation")
1482
        annotations = {}
1483
        for annotation_elem in annotation_elems:
1484
            lease_id = int(annotation_elem.get("id"))
1485
            annotations[lease_id] = LeaseAnnotation.from_xml_element(annotation_elem)
1486
            
1487
        attributes = {}
1488
        attributes_elem = element.find("attributes")
1489
        if attributes_elem != None:
1490
            for attr_elem in attributes_elem:
1491
                attributes[attr_elem.get("name")] = attr_elem.get("value")
1492
            
1493
        return cls(annotations, attributes)    
1494
    
1495
    def to_xml(self):
1496
        """Returns an ElementTree XML representation of the lease
1497
        
1498
        See the Haizea documentation for details on the
1499
        lease XML format.
1500
        
1501
        """        
1502
        annotations = ET.Element("lease-annotations")
1503

    
1504
        attributes = ET.SubElement(annotations, "attributes")
1505
        for name, value in self.attributes.items():
1506
            attr_elem = ET.SubElement(attributes, "attr")
1507
            attr_elem.set("name", name)
1508
            attr_elem.set("value", value)
1509

    
1510
        for annotation in self.annotations.values():
1511
            annotations.append(annotation.to_xml())
1512
            
1513
        return annotations
1514

    
1515
    def to_xml_string(self):
1516
        """Returns a string XML representation of the lease
1517
        
1518
        See the Haizea documentation for details on the
1519
        lease XML format.
1520
        
1521
        """   
1522
        return ET.tostring(self.to_xml())    
1523
        
1524
class Site(object):
1525
    """Represents a site containing machines ("nodes").
1526
    
1527
    This class is used to load site descriptions in XML format or
1528
    using a "resources string". Site descriptions can appear in two places:
1529
    in a LWF file (where the site required for the lease workload is
1530
    embedded in the LWF file) or in the Haizea configuration file. In both
1531
    cases, the site description is only used in simulation (in OpenNebula mode,
1532
    the available nodes and resources are obtained by querying OpenNebula). 
1533
    
1534
    Note that this class is distinct from the ResourcePool class, even though
1535
    both are used to represent "collections of nodes". The Site class is used
1536
    purely as a convenient way to load site information from an XML file
1537
    and to manipulate that information elsewhere in Haizea, while the
1538
    ResourcePool class is responsible for sending enactment commands
1539
    to nodes, monitoring nodes, etc.
1540
    """        
1541
    def __init__(self, nodes, resource_types, attr_types):
1542
        """Constructor.
1543
        
1544
        Arguments:
1545
        nodes -- A Nodes object
1546
        resource_types -- A list of valid resource types in this site.
1547
        attr_types -- A list of valid attribute types in this site
1548
        """             
1549
        self.nodes = nodes
1550
        self.resource_types = resource_types
1551
        self.attr_types = attr_types
1552
        
1553
    @classmethod
1554
    def from_xml_file(cls, xml_file):
1555
        """Constructs a site from an XML file.
1556
        
1557
        See the Haizea documentation for details on the
1558
        site XML format.
1559
        
1560
        Argument:
1561
        xml_file -- XML file containing the site in XML format.
1562
        """                
1563
        return cls.__from_xml_element(ET.parse(xml_file).getroot())        
1564

    
1565
    @classmethod
1566
    def from_lwf_file(cls, lwf_file):
1567
        """Constructs a site from an LWF file.
1568
        
1569
        LWF files can have site information embedded in them. This method
1570
        loads this site information from an LWF file. See the Haizea 
1571
        documentation for details on the LWF format.
1572
        
1573
        Argument:
1574
        lwf_file -- LWF file.
1575
        """                
1576
        site_elem = ET.parse(lwf_file).getroot().find("site")
1577
        if site_elem == None:
1578
            return None # LWF file does not contain a <site> element
1579
        else:
1580
            return cls.__from_xml_element(site_elem)        
1581
        
1582
    @classmethod
1583
    def __from_xml_element(cls, element):     
1584
        """Constructs a site from an ElementTree element.
1585
        
1586
        See the Haizea documentation for details on the
1587
        site XML format.
1588
        
1589
        Argument:
1590
        element -- Element object containing a "<site>" element.
1591
        """     
1592
        resource_types = element.find("resource-types")
1593
        resource_types = resource_types.get("names").split()
1594
       
1595
        # TODO: Attributes
1596
        attrs = []
1597
        
1598
        nodes = Nodes.from_xml_element(element.find("nodes"))
1599

    
1600
        # Validate nodes
1601
        for node_set in nodes.node_sets:
1602
            capacity = node_set[1]
1603
            for resource_type in capacity.get_resource_types():
1604
                if resource_type not in resource_types:
1605
                    # TODO: Raise something more meaningful
1606
                    raise Exception
1607

    
1608
        return cls(nodes, resource_types, attrs)
1609
    
1610
    @classmethod
1611
    def from_resources_string(cls, resource_str):
1612
        """Constructs a site from a "resources string"
1613
        
1614
        A "resources string" is a shorthand way of specifying a site
1615
        with homogeneous resources and no attributes. The format is:
1616
        
1617
        <numnodes> <resource_type>:<resource_quantity>[,<resource_type>:<resource_quantity>]*
1618
        
1619
        For example: 4 CPU:100,Memory:1024
1620
        
1621
        Argument:
1622
        resource_str -- resources string
1623
        """    
1624

    
1625
        resource_str = resource_str.split()
1626
        numnodes = int(resource_str[0])
1627
        resources = resource_str[1]
1628
        capacity = Capacity.from_resources_string(resources)
1629
        
1630
        nodes = Nodes([(numnodes,capacity)])
1631

    
1632
        return cls(nodes, capacity.get_resource_types(), [])
1633
            
1634
    def add_resource(self, name, amounts):
1635
        """Adds a new resource to all nodes in the site.
1636
                
1637
        Argument:
1638
        name -- Name of the resource type
1639
        amounts -- A list with the amounts of the resource to add to each
1640
        node. If the resource is single-instance, then this will just
1641
        be a list with a single element. If multi-instance, each element
1642
        of the list represent the amount of an instance of the resource.
1643
        """            
1644
        self.resource_types.append(name)
1645
        self.nodes.add_resource(name, amounts)
1646
    
1647
    def get_resource_types(self):
1648
        """Returns the resource types in this site.
1649
        
1650
        This method returns a list, each item being a pair with
1651
        1. the name of the resource type and 2. the maximum number of
1652
        instances for that resource type across all nodes.
1653
                
1654
        """               
1655
        max_ninstances = dict((rt, 1) for rt in self.resource_types)
1656
        for node_set in self.nodes.node_sets:
1657
            capacity = node_set[1]
1658
            for resource_type in capacity.get_resource_types():
1659
                if capacity.ninstances[resource_type] > max_ninstances[resource_type]:
1660
                    max_ninstances[resource_type] = capacity.ninstances[resource_type]
1661
                    
1662
        max_ninstances = [(rt,max_ninstances[rt]) for rt in self.resource_types]
1663

    
1664
        return max_ninstances
1665
    
1666
    def to_xml(self):
1667
        """Returns an ElementTree XML representation of the nodes
1668
        
1669
        See the Haizea documentation for details on the
1670
        lease XML format.
1671
        
1672
        """   
1673
        site = ET.Element("site")
1674
        resource_types = ET.SubElement(site, "resource-types")
1675
        resource_types.set("names", " ".join(self.resource_types))
1676
        site.append(self.nodes.to_xml())
1677
            
1678
        return site    
1679

    
1680

    
1681
class Nodes(object):
1682
    """Represents a collection of machines ("nodes")
1683
    
1684
    This class is used to load descriptions of nodes from an XML
1685
    file. These nodes can appear in two places: in a site description
1686
    (which, in turn, is loaded by the Site class) or in a lease's
1687
    resource requirements (describing what nodes, with what resources,
1688
    are required by the lease).
1689
    
1690
    Nodes are stored as one or more "node sets". Each node set has nodes
1691
    with the exact same resources. So, for example, a lease requiring 100
1692
    nodes (all identical, except 50 have 1024MB of memory and the other 50
1693
    have 512MB of memory) doesn't need to enumerate all 100 nodes. Instead,
1694
    it just has to describe the two "node sets" (indicating that there are
1695
    50 nodes of one type and 50 of the other). See the Haizea documentation
1696
    for more details on the XML format.
1697
    
1698
    Like the Site class, this class is distinct from the ResourcePool class, even
1699
    though they both represent a "collection of nodes". See the 
1700
    Site class documentation for more details.
1701
    """            
1702
    def __init__(self, node_sets):
1703
        """Constructor.
1704
        
1705
        Arguments:
1706
        node_sets -- A list of (n,c) pairs (where n is the number of nodes
1707
        in the set and c is a Capacity object; all nodes in the set have
1708
        capacity c).
1709
        """                 
1710
        self.node_sets = node_sets
1711

    
1712
    @classmethod
1713
    def from_xml_element(cls, nodes_element):
1714
        """Constructs a node collection from an ElementTree element.
1715
        
1716
        See the Haizea documentation for details on the
1717
        <nodes> XML format.
1718
        
1719
        Argument:
1720
        element -- Element object containing a "<nodes>" element.
1721
        """           
1722
        nodesets = []
1723
        nodesets_elems = nodes_element.findall("node-set")
1724
        for nodeset_elem in nodesets_elems:
1725
            r = Capacity([])
1726
            resources = nodeset_elem.findall("res")
1727
            for i, res in enumerate(resources):
1728
                res_type = res.get("type")
1729
                if len(res.getchildren()) == 0:
1730
                    amount = int(res.get("amount"))
1731
                    r.set_ninstances(res_type, 1)
1732
                    r.set_quantity(res_type, amount)
1733
                else:
1734
                    instances = res.findall("instance")
1735
                    r.set_ninstances(res_type, len(instances))
1736
                    for i, instance in enumerate(instances):
1737
                        amount = int(instance.get("amount"))
1738
                        r.set_quantity_instance(res_type, i+1, amount)
1739
                                     
1740
            numnodes = int(nodeset_elem.get("numnodes"))
1741

    
1742
            nodesets.append((numnodes,r))
1743
            
1744
        return cls(nodesets)
1745
    
1746
    def to_xml(self):
1747
        """Returns an ElementTree XML representation of the nodes
1748
        
1749
        See the Haizea documentation for details on the
1750
        lease XML format.
1751
        
1752
        """   
1753
        nodes = ET.Element("nodes")
1754
        for (numnodes, capacity) in self.node_sets:
1755
            nodeset = ET.SubElement(nodes, "node-set")
1756
            nodeset.set("numnodes", str(numnodes))
1757
            for res_type in capacity.get_resource_types():
1758
                res = ET.SubElement(nodeset, "res")
1759
                res.set("type", res_type)
1760
                ninstances = capacity.get_ninstances(res_type)
1761
                if ninstances == 1:
1762
                    res.set("amount", str(capacity.get_quantity(res_type)))                
1763
            
1764
        return nodes
1765
    
1766
    def get_all_nodes(self):
1767
        """Returns a dictionary mapping individual nodes to capacities
1768
        
1769
        """              
1770
        nodes = {}
1771
        nodenum = 1
1772
        for node_set in self.node_sets:
1773
            numnodes = node_set[0]
1774
            r = node_set[1]
1775
            for i in range(numnodes):
1776
                nodes[nodenum] = r
1777
                nodenum += 1     
1778
        return nodes   
1779
                
1780
    def add_resource(self, name, amounts):
1781
        """Adds a new resource to all the nodes
1782
                
1783
        Argument:
1784
        name -- Name of the resource type
1785
        amounts -- A list with the amounts of the resource to add to each
1786
        node. If the resource is single-instance, then this will just
1787
        be a list with a single element. If multi-instance, each element
1788
        of the list represent the amount of an instance of the resource.
1789
        """              
1790
        for node_set in self.node_sets:
1791
            r = node_set[1]
1792
            r.set_ninstances(name, len(amounts))
1793
            for ninstance, amount in enumerate(amounts):
1794
                r.set_quantity_instance(name, ninstance+1, amount)
1795