Project

General

Profile

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

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
        if vmrr == None:
514
            # Nothing scheduled, no endtime
515
            return None
516
        else:
517
            return vmrr.get_final_end()
518
    
519
    def get_accumulated_duration_at(self, time):
520
        """Returns the amount of time required to fulfil the entire
521
        requested duration of the lease at a given time.
522
                        
523
        """
524
        t = TimeDelta(0)
525
        for vmrr in self.vm_rrs:
526
            if time >= vmrr.end:
527
                t += vmrr.end - vmrr.start
528
            elif time >= vmrr.start and time < vmrr.end:
529
                t += time -vmrr.start
530
                break
531
            else:
532
                break
533
        return t
534

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

    
582
    def clear_rrs(self):
583
        """Removes all resource reservations for this lease
584
        (both preparation and VM)
585
        
586
        """            
587
        self.preparation_rrs = []
588

    
589
        for rr in self.vm_rrs: 
590
            rr.clear_rrs()
591
        self.vm_rrs = []
592

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

    
656

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

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

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

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

    
747
            for post_rr in vmrr.post_rrs:
748
                assert post_rr.start >= prev_time
749
                assert post_rr.end >= post_rr.start
750
                prev_time = post_rr.end
751
                
752
            prev_vmrr = vmrr
753
            
754
        if len(self.preparation_rrs) > 0 and len(self.vm_rrs) > 0:
755
            assert self.preparation_rrs[-1].end <= self.vm_rrs[0].start
756
            
757
            
758
    def __estimate_suspend_resume_time(self, rate):
759
        """ Estimate the time to suspend/resume an entire lease
760
                            
761
        Note that, unlike __compute_suspend_resume_time, this estimates
762
        the time to suspend/resume an entire lease (which may involve
763
        suspending several VMs)
764
        
765
        Arguments:
766
        lease -- Lease that is going to be suspended/resumed
767
        rate -- The rate at which an individual VM is suspended/resumed
768
        
769
        """              
770
        susp_exclusion = get_config().get("suspendresume-exclusion")        
771
        enactment_overhead = get_config().get("enactment-overhead") 
772
        time = 0
773
        for vnode in self.requested_resources:
774
            mem = self.requested_resources[vnode].get_quantity(RES_MEM)
775
            # Overestimating when susp_exclusion == SUSPRES_EXCLUSION_LOCAL
776
            time += compute_suspend_resume_time(mem, rate) + enactment_overhead
777
        return time
778

    
779
    def __repr__(self):
780
        """Returns a string representation of the Lease"""
781
        return "L%i" % self.id
782
        
783
    # ONLY for simulation
784
    def _update_prematureend(self):
785
        known = self.duration.known
786
        acc = TimeDelta(0)
787
        for vmrr in self.vm_rrs:
788
            if known != None:
789
                rrdur = vmrr.end - vmrr.start
790
                if known - acc <= rrdur:
791
                    vmrr.prematureend = vmrr.start + (known-acc)
792
                    break                 
793
                else:
794
                    vmrr.prematureend = None
795
                    acc += rrdur
796
            else:
797
                vmrr.prematureend = None
798
        
799
        
800
class LeaseStateMachine(StateMachine):
801
    """A lease state machine
802
    
803
    A child of StateMachine, this class simply specifies the valid
804
    states and transitions for a lease (the actual state machine code
805
    is in StateMachine).
806
    
807
    See the Haizea documentation for a description of states and
808
    valid transitions.
809
    
810
    """
811
    transitions = {Lease.STATE_NEW:                 [(Lease.STATE_PENDING,    "")],
812
                   
813
                   Lease.STATE_PENDING:             [(Lease.STATE_SCHEDULED,  ""),
814
                                                     (Lease.STATE_QUEUED,     ""),
815
                                                     (Lease.STATE_CANCELLED,  ""),
816
                                                     (Lease.STATE_REJECTED,   ""),
817
                                                     (Lease.STATE_REJECTED_BY_USER,   "")],
818
                                                     
819
                   Lease.STATE_SCHEDULED:           [(Lease.STATE_PREPARING,  ""),
820
                                                     (Lease.STATE_QUEUED,     ""),
821
                                                     (Lease.STATE_PENDING,    ""),
822
                                                     (Lease.STATE_READY,      ""),
823
                                                     (Lease.STATE_CANCELLED,  ""),
824
                                                     (Lease.STATE_FAIL,       "")],
825
                                                     
826
                   Lease.STATE_QUEUED:              [(Lease.STATE_SCHEDULED,  ""),
827
                                                     (Lease.STATE_CANCELLED,  "")],
828
                                                     
829
                   Lease.STATE_PREPARING:           [(Lease.STATE_READY,      ""),
830
                                                     (Lease.STATE_QUEUED,     ""),
831
                                                     (Lease.STATE_PENDING,    ""),
832
                                                     (Lease.STATE_CANCELLED,  ""),
833
                                                     (Lease.STATE_FAIL,       "")],
834
                                                     
835
                   Lease.STATE_READY:               [(Lease.STATE_ACTIVE,     ""),
836
                                                     (Lease.STATE_QUEUED,     ""),
837
                                                     (Lease.STATE_PENDING,     ""),
838
                                                     (Lease.STATE_CANCELLED,  ""),
839
                                                     (Lease.STATE_FAIL,       "")],
840
                                                     
841
                   Lease.STATE_ACTIVE:              [(Lease.STATE_SUSPENDING, ""),
842
                                                     (Lease.STATE_READY,     ""),
843
                                                     (Lease.STATE_QUEUED,     ""),
844
                                                     (Lease.STATE_DONE,       ""),
845
                                                     (Lease.STATE_CANCELLED,  ""),
846
                                                     (Lease.STATE_FAIL,       "")],
847
                                                     
848
                   Lease.STATE_SUSPENDING:          [(Lease.STATE_SUSPENDED_PENDING,  ""),
849
                                                     (Lease.STATE_CANCELLED,  ""),
850
                                                     (Lease.STATE_FAIL,       "")],
851
                                                     
852
                   Lease.STATE_SUSPENDED_PENDING:   [(Lease.STATE_SUSPENDED_QUEUED,     ""),
853
                                                     (Lease.STATE_SUSPENDED_SCHEDULED,  ""),
854
                                                     (Lease.STATE_CANCELLED,  ""),
855
                                                     (Lease.STATE_FAIL,       "")],
856
                                                     
857
                   Lease.STATE_SUSPENDED_QUEUED:    [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
858
                                                     (Lease.STATE_CANCELLED,  ""),
859
                                                     (Lease.STATE_FAIL,       "")],
860
                                                     
861
                   Lease.STATE_SUSPENDED_SCHEDULED: [(Lease.STATE_SUSPENDED_QUEUED,     ""),
862
                                                     (Lease.STATE_SUSPENDED_PENDING,  ""),
863
                                                     (Lease.STATE_MIGRATING,  ""),
864
                                                     (Lease.STATE_RESUMING,   ""),
865
                                                     (Lease.STATE_CANCELLED,  ""),
866
                                                     (Lease.STATE_FAIL,       "")],
867
                                                     
868
                   Lease.STATE_MIGRATING:           [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
869
                                                     (Lease.STATE_CANCELLED,  ""),
870
                                                     (Lease.STATE_FAIL,       "")],
871
                                                     
872
                   Lease.STATE_RESUMING:            [(Lease.STATE_RESUMED_READY, ""),
873
                                                     (Lease.STATE_CANCELLED,  ""),
874
                                                     (Lease.STATE_FAIL,       "")],
875
                                                     
876
                   Lease.STATE_RESUMED_READY:       [(Lease.STATE_ACTIVE,     ""),
877
                                                     (Lease.STATE_CANCELLED,  ""),
878
                                                     (Lease.STATE_FAIL,       "")],
879
                   
880
                   # Final states
881
                   Lease.STATE_DONE:          [],
882
                   Lease.STATE_CANCELLED:     [],
883
                   Lease.STATE_FAIL:          [],
884
                   Lease.STATE_REJECTED:      [],
885
                   }
886
    
887
    def __init__(self, initial_state):
888
        StateMachine.__init__(self, initial_state, LeaseStateMachine.transitions, Lease.state_str)
889

    
890

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

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

    
1025
        return capacity
1026
    
1027
    def __eq__(self, other):
1028
        """Tests if two capacities are the same
1029
                        
1030
        """        
1031
        for res_type in self.quantity:
1032
            if not other.quantity.has_key(res_type):
1033
                return False
1034
            if self.ninstances[res_type] != other.ninstances[res_type]:
1035
                return False
1036
            if self.quantity[res_type] != other.quantity[res_type]:
1037
                return False
1038
        return True
1039

    
1040
    def __ne__(self, other):
1041
        """Tests if two capacities are not the same
1042
                        
1043
        """        
1044
        return not self == other
1045
            
1046
    def __repr__(self):
1047
        """Returns a string representation of the Capacity"""
1048
        return "  |  ".join("%s: %s" % (res_type,q) for res_type, q in self.quantity.items())
1049
            
1050

    
1051
class Timestamp(object):
1052
    """An exact point in time.
1053
    
1054
    This class is just a wrapper around three DateTimes. When
1055
    dealing with timestamps in Haizea (such as the requested
1056
    starting time for a lease), we want to keep track not just
1057
    of the requested timestamp, but also the scheduled timestamp
1058
    (which could differ from the requested one) and the
1059
    actual timestamp (which could differ from the scheduled one).
1060
    """        
1061
    
1062
    UNSPECIFIED = "Unspecified"
1063
    NOW = "Now"
1064
    
1065
    def __init__(self, requested):
1066
        """Constructor
1067
                        
1068
        Argument:
1069
        requested -- The requested timestamp
1070
        """        
1071
        self.requested = requested
1072
        self.scheduled = None
1073
        self.actual = None
1074

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

    
1153
    def get_remaining_known_duration(self):
1154
        """Returns the amount of time required to fulfil the entire
1155
        known duration of the lease.
1156
              
1157
        ONLY for simulations.
1158
        """
1159
        return self.known - self.accumulated
1160
            
1161
    def __repr__(self):
1162
        """Returns a string representation of the Duration"""
1163
        return "REQ: %s  |  ACC: %s  |  ACT: %s  |  KNW: %s" % (self.requested, self.accumulated, self.actual, self.known)
1164
    
1165
class SoftwareEnvironment(object):
1166
    """The base class for a lease's software environment"""
1167
    
1168
    def __init__(self):
1169
        """Constructor.
1170
        
1171
        Does nothing."""
1172
        pass
1173

    
1174
class UnmanagedSoftwareEnvironment(SoftwareEnvironment):
1175
    """Represents an "unmanaged" software environment.
1176
    
1177
    When a lease has an unmanaged software environment,
1178
    Haizea does not need to perform any actions to prepare
1179
    a lease's software environment (it assumes that this
1180
    task is carried out by an external entity, and that
1181
    software environments can be assumed to be ready
1182
    when a lease has to start; e.g., if VM disk images are
1183
    predeployed on all physical nodes)."""
1184
    
1185
    def __init__(self):
1186
        """Constructor.
1187
        
1188
        Does nothing."""        
1189
        SoftwareEnvironment.__init__(self)
1190

    
1191
class DiskImageSoftwareEnvironment(SoftwareEnvironment):
1192
    """Reprents a software environment encapsulated in a disk image.
1193
    
1194
    When a lease's software environment is contained in a disk image,
1195
    this disk image must be deployed to the physical nodes the lease
1196
    is mapped to before the lease can start. This means that the
1197
    preparation for this lease must be handled by a preparation
1198
    scheduler (see documentation in lease_scheduler) capable of
1199
    handling a DiskImageSoftwareEnvironment.
1200
    """
1201
    def __init__(self, image_id, image_size):
1202
        """Constructor.
1203
        
1204
        Arguments:
1205
        image_id -- A unique identifier for the disk image required
1206
        by the lease.
1207
        image_size -- The size, in MB, of the disk image. """         
1208
        self.image_id = image_id
1209
        self.image_size = image_size
1210
        SoftwareEnvironment.__init__(self)
1211

    
1212

    
1213
    
1214
class LeaseWorkload(object):
1215
    """Reprents a sequence of lease requests.
1216
    
1217
    A lease workload is a sequence of lease requests with a specific
1218
    arrival time for each lease. This class is currently only used
1219
    to load LWF (Lease Workload File) files. See the Haizea documentation 
1220
    for details on the LWF format.
1221
    """    
1222
    def __init__(self, leases):
1223
        """Constructor.
1224
        
1225
        Arguments:
1226
        leases -- An ordered list (by arrival time) of leases in the workload
1227
        """                 
1228
        self.leases = leases
1229
        
1230

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

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

    
1280
            # If a deadline is specified, add the init time
1281
            deadline = lease.find("deadline")
1282
            if deadline != None:
1283
                t = inittime + Parser.DateTimeDeltaFromString(deadline.get("time"))
1284
                deadline.set("time", str(t))
1285
                
1286
            lease = Lease.create_new_from_xml_element(lease)
1287
            
1288
            realduration = r.find("realduration")
1289
            if realduration != None:
1290
                realduration = Parser.DateTimeDeltaFromString(realduration.get("time"))
1291
                if realduration < lease.duration.requested:
1292
                    lease.duration.known = realduration
1293

    
1294
            leases.append(lease)
1295
            
1296
        return cls(leases)
1297
    
1298
class LeaseAnnotation(object):
1299
    """Represents a lease annotation.
1300
    
1301
    ...
1302
    """    
1303
    def __init__(self, lease_id, start, deadline, software, extras):
1304
        """Constructor.
1305
        
1306
        Arguments:
1307
        ...
1308
        """                 
1309
        self.lease_id = lease_id
1310
        self.start = start
1311
        self.deadline = deadline
1312
        self.software = software
1313
        self.extras = extras
1314
        
1315
    
1316
    @classmethod
1317
    def from_xml_file(cls, xml_file):
1318
        """...
1319
        
1320
        ...
1321
        
1322
        Argument:
1323
        xml_file -- XML file containing the lease in XML format.
1324
        """        
1325
        return cls.__from_xml_element(ET.parse(xml_file).getroot())
1326

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

    
1360
        
1361
        software = element.find("software")
1362

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

    
1417
    def to_xml_string(self):
1418
        """Returns a string XML representation of the lease annotation
1419
        
1420
        ...
1421
        
1422
        """   
1423
        return ET.tostring(self.to_xml())    
1424
    
1425
class LeaseAnnotations(object):
1426
    """Represents a sequence of lease annotations.
1427
    
1428
    ...
1429
    """    
1430
    def __init__(self, annotations, attributes):
1431
        """Constructor.
1432
        
1433
        Arguments:
1434
        annotations -- A dictionary of annotations
1435
        """
1436
        if isinstance(annotations, list):
1437
            self.lease_specific_annotations = False
1438
        elif isinstance(annotations, dict):
1439
            self.lease_specific_annotations = True
1440
        self.annotations = annotations
1441
        self.attributes = attributes
1442
    
1443
    def __apply_to_lease(self, lease, annotation):
1444
        if annotation.start != None:
1445
            if annotation.start.requested in (Timestamp.NOW, Timestamp.UNSPECIFIED):
1446
                lease.start.requested = annotation.start.requested
1447
            else:
1448
                lease.start.requested = lease.submit_time + annotation.start.requested
1449

    
1450
        if annotation.deadline != None:
1451
            lease.deadline = lease.submit_time + annotation.deadline
1452

    
1453
        if annotation.software != None:
1454
            lease.software = annotation.software
1455

    
1456
        if annotation.extras != None:
1457
            lease.extras.update(annotation.extras)
1458
    
1459
    def apply_to_leases(self, leases):
1460
        """Apply annotations to a workload
1461
        
1462
        """
1463
        if self.lease_specific_annotations:
1464
            for lease in [l for l in leases if self.annotations.has_key(l.id)]:
1465
                annotation = self.annotations[lease.id]
1466
                self.__apply_to_lease(lease, annotation)
1467
        else:
1468
            for lease, annotation in zip(leases, self.annotations):
1469
                self.__apply_to_lease(lease, annotation)            
1470
    
1471
    @classmethod
1472
    def from_xml_file(cls, xml_file):
1473
        """...
1474
        
1475
        ...
1476
        
1477
        Argument:
1478
        xml_file -- XML file containing the lease in XML format.
1479
        """        
1480
        return cls.__from_xml_element(ET.parse(xml_file).getroot())
1481

    
1482
    @classmethod
1483
    def __from_xml_element(cls, element):
1484
        """...
1485
        
1486
        ...
1487
        
1488
        Argument:
1489
        element -- Element object containing a "<lease-annotations>" element.
1490
        """                
1491
        annotation_elems = element.findall("lease-annotation")
1492
        annotations_dict = {}
1493
        annotations_list = []
1494
        for annotation_elem in annotation_elems:
1495
            annotation = LeaseAnnotation.from_xml_element(annotation_elem)
1496
            if annotation.lease_id == None:
1497
                annotations_list.append(annotation)
1498
            else:
1499
                annotations_dict[int(annotation.lease_id)] = annotation
1500
            
1501
        attributes = {}
1502
        attributes_elem = element.find("attributes")
1503
        if attributes_elem != None:
1504
            for attr_elem in attributes_elem:
1505
                attributes[attr_elem.get("name")] = attr_elem.get("value")
1506

    
1507
        if len(annotations_list) != 0 and len(annotations_dict) != 0:
1508
            raise Exception #TODO: raise something more meaningful
1509
        elif len(annotations_list) == 0:
1510
            annotations = annotations_dict
1511
        elif len(annotations_dict) == 0:
1512
            annotations = annotations_list
1513

    
1514
        return cls(annotations, attributes)    
1515
    
1516
    def to_xml(self):
1517
        """Returns an ElementTree XML representation of the lease
1518
        
1519
        See the Haizea documentation for details on the
1520
        lease XML format.
1521
        
1522
        """        
1523
        annotations_elem = ET.Element("lease-annotations")
1524

    
1525
        attributes = ET.SubElement(annotations_elem, "attributes")
1526
        for name, value in self.attributes.items():
1527
            attr_elem = ET.SubElement(attributes, "attr")
1528
            attr_elem.set("name", name)
1529
            attr_elem.set("value", value)
1530

    
1531
        if self.lease_specific_annotations:
1532
            annotations = self.annotations.values()
1533
        else:
1534
            annotations = self.annotations
1535

    
1536
        for annotation in annotations:
1537
            annotations_elem.append(annotation.to_xml())
1538
            
1539
        return annotations_elem
1540

    
1541
    def to_xml_string(self):
1542
        """Returns a string XML representation of the lease
1543
        
1544
        See the Haizea documentation for details on the
1545
        lease XML format.
1546
        
1547
        """   
1548
        return ET.tostring(self.to_xml())    
1549
        
1550
class Site(object):
1551
    """Represents a site containing machines ("nodes").
1552
    
1553
    This class is used to load site descriptions in XML format or
1554
    using a "resources string". Site descriptions can appear in two places:
1555
    in a LWF file (where the site required for the lease workload is
1556
    embedded in the LWF file) or in the Haizea configuration file. In both
1557
    cases, the site description is only used in simulation (in OpenNebula mode,
1558
    the available nodes and resources are obtained by querying OpenNebula). 
1559
    
1560
    Note that this class is distinct from the ResourcePool class, even though
1561
    both are used to represent "collections of nodes". The Site class is used
1562
    purely as a convenient way to load site information from an XML file
1563
    and to manipulate that information elsewhere in Haizea, while the
1564
    ResourcePool class is responsible for sending enactment commands
1565
    to nodes, monitoring nodes, etc.
1566
    """        
1567
    def __init__(self, nodes, resource_types, attr_types):
1568
        """Constructor.
1569
        
1570
        Arguments:
1571
        nodes -- A Nodes object
1572
        resource_types -- A list of valid resource types in this site.
1573
        attr_types -- A list of valid attribute types in this site
1574
        """             
1575
        self.nodes = nodes
1576
        self.resource_types = resource_types
1577
        self.attr_types = attr_types
1578
        
1579
    @classmethod
1580
    def from_xml_file(cls, xml_file):
1581
        """Constructs a site from an XML file.
1582
        
1583
        See the Haizea documentation for details on the
1584
        site XML format.
1585
        
1586
        Argument:
1587
        xml_file -- XML file containing the site in XML format.
1588
        """                
1589
        return cls.__from_xml_element(ET.parse(xml_file).getroot())        
1590

    
1591
    @classmethod
1592
    def from_lwf_file(cls, lwf_file):
1593
        """Constructs a site from an LWF file.
1594
        
1595
        LWF files can have site information embedded in them. This method
1596
        loads this site information from an LWF file. See the Haizea 
1597
        documentation for details on the LWF format.
1598
        
1599
        Argument:
1600
        lwf_file -- LWF file.
1601
        """                
1602
        site_elem = ET.parse(lwf_file).getroot().find("site")
1603
        if site_elem == None:
1604
            return None # LWF file does not contain a <site> element
1605
        else:
1606
            return cls.__from_xml_element(site_elem)        
1607
        
1608
    @classmethod
1609
    def __from_xml_element(cls, element):     
1610
        """Constructs a site from an ElementTree element.
1611
        
1612
        See the Haizea documentation for details on the
1613
        site XML format.
1614
        
1615
        Argument:
1616
        element -- Element object containing a "<site>" element.
1617
        """     
1618
        resource_types = element.find("resource-types")
1619
        resource_types = resource_types.get("names").split()
1620
       
1621
        # TODO: Attributes
1622
        attrs = []
1623
        
1624
        nodes = Nodes.from_xml_element(element.find("nodes"))
1625

    
1626
        # Validate nodes
1627
        for node_set in nodes.node_sets:
1628
            capacity = node_set[1]
1629
            for resource_type in capacity.get_resource_types():
1630
                if resource_type not in resource_types:
1631
                    # TODO: Raise something more meaningful
1632
                    raise Exception
1633

    
1634
        return cls(nodes, resource_types, attrs)
1635
    
1636
    @classmethod
1637
    def from_resources_string(cls, resource_str):
1638
        """Constructs a site from a "resources string"
1639
        
1640
        A "resources string" is a shorthand way of specifying a site
1641
        with homogeneous resources and no attributes. The format is:
1642
        
1643
        <numnodes> <resource_type>:<resource_quantity>[,<resource_type>:<resource_quantity>]*
1644
        
1645
        For example: 4 CPU:100,Memory:1024
1646
        
1647
        Argument:
1648
        resource_str -- resources string
1649
        """    
1650

    
1651
        resource_str = resource_str.split()
1652
        numnodes = int(resource_str[0])
1653
        resources = resource_str[1]
1654
        capacity = Capacity.from_resources_string(resources)
1655
        
1656
        nodes = Nodes([(numnodes,capacity)])
1657

    
1658
        return cls(nodes, capacity.get_resource_types(), [])
1659
            
1660
    def add_resource(self, name, amounts):
1661
        """Adds a new resource to all nodes in the site.
1662
                
1663
        Argument:
1664
        name -- Name of the resource type
1665
        amounts -- A list with the amounts of the resource to add to each
1666
        node. If the resource is single-instance, then this will just
1667
        be a list with a single element. If multi-instance, each element
1668
        of the list represent the amount of an instance of the resource.
1669
        """            
1670
        self.resource_types.append(name)
1671
        self.nodes.add_resource(name, amounts)
1672
    
1673
    def get_resource_types(self):
1674
        """Returns the resource types in this site.
1675
        
1676
        This method returns a list, each item being a pair with
1677
        1. the name of the resource type and 2. the maximum number of
1678
        instances for that resource type across all nodes.
1679
                
1680
        """               
1681
        max_ninstances = dict((rt, 1) for rt in self.resource_types)
1682
        for node_set in self.nodes.node_sets:
1683
            capacity = node_set[1]
1684
            for resource_type in capacity.get_resource_types():
1685
                if capacity.ninstances[resource_type] > max_ninstances[resource_type]:
1686
                    max_ninstances[resource_type] = capacity.ninstances[resource_type]
1687
                    
1688
        max_ninstances = [(rt,max_ninstances[rt]) for rt in self.resource_types]
1689

    
1690
        return max_ninstances
1691
    
1692
    def to_xml(self):
1693
        """Returns an ElementTree XML representation of the nodes
1694
        
1695
        See the Haizea documentation for details on the
1696
        lease XML format.
1697
        
1698
        """   
1699
        site = ET.Element("site")
1700
        resource_types = ET.SubElement(site, "resource-types")
1701
        resource_types.set("names", " ".join(self.resource_types))
1702
        site.append(self.nodes.to_xml())
1703
            
1704
        return site    
1705

    
1706

    
1707
class Nodes(object):
1708
    """Represents a collection of machines ("nodes")
1709
    
1710
    This class is used to load descriptions of nodes from an XML
1711
    file. These nodes can appear in two places: in a site description
1712
    (which, in turn, is loaded by the Site class) or in a lease's
1713
    resource requirements (describing what nodes, with what resources,
1714
    are required by the lease).
1715
    
1716
    Nodes are stored as one or more "node sets". Each node set has nodes
1717
    with the exact same resources. So, for example, a lease requiring 100
1718
    nodes (all identical, except 50 have 1024MB of memory and the other 50
1719
    have 512MB of memory) doesn't need to enumerate all 100 nodes. Instead,
1720
    it just has to describe the two "node sets" (indicating that there are
1721
    50 nodes of one type and 50 of the other). See the Haizea documentation
1722
    for more details on the XML format.
1723
    
1724
    Like the Site class, this class is distinct from the ResourcePool class, even
1725
    though they both represent a "collection of nodes". See the 
1726
    Site class documentation for more details.
1727
    """            
1728
    def __init__(self, node_sets):
1729
        """Constructor.
1730
        
1731
        Arguments:
1732
        node_sets -- A list of (n,c) pairs (where n is the number of nodes
1733
        in the set and c is a Capacity object; all nodes in the set have
1734
        capacity c).
1735
        """                 
1736
        self.node_sets = node_sets
1737

    
1738
    @classmethod
1739
    def from_xml_element(cls, nodes_element):
1740
        """Constructs a node collection from an ElementTree element.
1741
        
1742
        See the Haizea documentation for details on the
1743
        <nodes> XML format.
1744
        
1745
        Argument:
1746
        element -- Element object containing a "<nodes>" element.
1747
        """           
1748
        nodesets = []
1749
        nodesets_elems = nodes_element.findall("node-set")
1750
        for nodeset_elem in nodesets_elems:
1751
            r = Capacity([])
1752
            resources = nodeset_elem.findall("res")
1753
            for i, res in enumerate(resources):
1754
                res_type = res.get("type")
1755
                if len(res.getchildren()) == 0:
1756
                    amount = int(res.get("amount"))
1757
                    r.set_ninstances(res_type, 1)
1758
                    r.set_quantity(res_type, amount)
1759
                else:
1760
                    instances = res.findall("instance")
1761
                    r.set_ninstances(res_type, len(instances))
1762
                    for i, instance in enumerate(instances):
1763
                        amount = int(instance.get("amount"))
1764
                        r.set_quantity_instance(res_type, i+1, amount)
1765
                                     
1766
            numnodes = int(nodeset_elem.get("numnodes"))
1767

    
1768
            nodesets.append((numnodes,r))
1769
            
1770
        return cls(nodesets)
1771
    
1772
    def to_xml(self):
1773
        """Returns an ElementTree XML representation of the nodes
1774
        
1775
        See the Haizea documentation for details on the
1776
        lease XML format.
1777
        
1778
        """   
1779
        nodes = ET.Element("nodes")
1780
        for (numnodes, capacity) in self.node_sets:
1781
            nodeset = ET.SubElement(nodes, "node-set")
1782
            nodeset.set("numnodes", str(numnodes))
1783
            for res_type in capacity.get_resource_types():
1784
                res = ET.SubElement(nodeset, "res")
1785
                res.set("type", res_type)
1786
                ninstances = capacity.get_ninstances(res_type)
1787
                if ninstances == 1:
1788
                    res.set("amount", str(capacity.get_quantity(res_type)))
1789
                else:
1790
                    for instance in range(1,ninstances+1):
1791
                        inst_elem = ET.SubElement(res, "instance")               
1792
                        inst_elem.set("amount", str(capacity.get_quantity_instance(res_type, instance)))
1793
            
1794
        return nodes
1795
    
1796
    def get_all_nodes(self):
1797
        """Returns a dictionary mapping individual nodes to capacities
1798
        
1799
        """              
1800
        nodes = {}
1801
        nodenum = 1
1802
        for node_set in self.node_sets:
1803
            numnodes = node_set[0]
1804
            r = node_set[1]
1805
            for i in range(numnodes):
1806
                nodes[nodenum] = r
1807
                nodenum += 1     
1808
        return nodes   
1809
                
1810
    def add_resource(self, name, amounts):
1811
        """Adds a new resource to all the nodes
1812
                
1813
        Argument:
1814
        name -- Name of the resource type
1815
        amounts -- A list with the amounts of the resource to add to each
1816
        node. If the resource is single-instance, then this will just
1817
        be a list with a single element. If multi-instance, each element
1818
        of the list represent the amount of an instance of the resource.
1819
        """              
1820
        for node_set in self.node_sets:
1821
            r = node_set[1]
1822
            r.set_ninstances(name, len(amounts))
1823
            for ninstance, amount in enumerate(amounts):
1824
                r.set_quantity_instance(name, ninstance+1, amount)
1825