Project

General

Profile

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

1 632 borja
# -------------------------------------------------------------------------- #
2 641 borja
# Copyright 2006-2009, University of Chicago                                 #
3
# Copyright 2008-2009, Distributed Systems Architecture Group, Universidad   #
4 632 borja
# 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 741 borja
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 632 borja
from haizea.core.scheduler.slottable import ResourceReservation
39
40
from mx.DateTime import DateTime, TimeDelta, Parser
41
42
import logging
43
44 675 borja
try:
45
    import xml.etree.ElementTree as ET
46
except ImportError:
47
    # Compatibility with Python <=2.4
48
    import elementtree.ElementTree as ET
49 632 borja
50
51
52 675 borja
53 632 borja
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 715 borja
    STATE_REJECTED_BY_USER = 18
89 632 borja
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 715 borja
                 STATE_FAIL : "Fail",
109
                 STATE_REJECTED_BY_USER : "Rejected by user"}
110 632 borja
111
    # Lease types
112
    BEST_EFFORT = 1
113
    ADVANCE_RESERVATION = 2
114
    IMMEDIATE = 3
115 683 borja
    DEADLINE = 4
116 632 borja
    UNKNOWN = -1
117
118
    # String representation of lease types
119
    type_str = {BEST_EFFORT: "Best-effort",
120
                ADVANCE_RESERVATION: "AR",
121
                IMMEDIATE: "Immediate",
122 683 borja
                DEADLINE: "Deadline",
123 632 borja
                UNKNOWN: "Unknown"}
124
125 695 borja
    def __init__(self, lease_id, submit_time, user_id, requested_resources, start, duration,
126 691 borja
                 deadline, preemptible, software, state, extras = {}):
127 632 borja
        """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 691 borja
        extras -- Extra attributes. Haizea will ignore them, but they
152
          may be used by pluggable modules.
153 632 borja
        """
154
        # Lease ID (read only)
155 658 borja
        self.id = lease_id
156 632 borja
157
        # Lease attributes
158
        self.submit_time = submit_time
159 695 borja
        self.user_id = user_id
160 632 borja
        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 691 borja
        self.price = None
167
        self.extras = extras
168 632 borja
169
        # Bookkeeping attributes:
170
171
        # Lease state
172
        if state == None:
173
            state = Lease.STATE_NEW
174 771 borja
        self.state_machine = LeaseStateMachine(initial_state = state)
175 632 borja
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 695 borja
    def create_new(cls, submit_time, user_id, requested_resources, start, duration,
201 632 borja
                 deadline, preemptible, software):
202 658 borja
        lease_id = get_lease_id()
203 632 borja
        state = Lease.STATE_NEW
204 695 borja
        return cls(lease_id, submit_time, user_id, requested_resources, start, duration,
205 632 borja
                 deadline, preemptible, software, state)
206
207
    @classmethod
208
    def create_new_from_xml_element(cls, element):
209
        lease = cls.from_xml_element(element)
210 722 borja
        if lease.id == None:
211
            lease.id = get_lease_id()
212 771 borja
        lease.state_machine = LeaseStateMachine(initial_state = Lease.STATE_NEW)
213 632 borja
        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 658 borja
        lease_id = element.get("id")
251 632 borja
252 658 borja
        if lease_id == None:
253
            lease_id = None
254 632 borja
        else:
255 658 borja
            lease_id = int(lease_id)
256 632 borja
257 695 borja
        user_id = element.get("user")
258
        if user_id == None:
259
            user_id = None
260
        else:
261 696 borja
            user_id = int(user_id)
262 695 borja
263 632 borja
        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 683 borja
        deadline = element.find("deadline")
293 632 borja
294 683 borja
        if deadline != None:
295
            deadline = Parser.DateTimeFromString(deadline.get("time"))
296
297 691 borja
        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 632 borja
        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 695 borja
        return Lease(lease_id, submit_time, user_id, requested_resources, start, duration,
321 691 borja
                     deadline, preemptible, software, state, extras)
322 632 borja
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 683 borja
            if self.deadline == None:
397
                return Lease.ADVANCE_RESERVATION
398
            else:
399
                return Lease.DEADLINE
400 632 borja
401
    def get_state(self):
402
        """Returns the lease's state.
403

404
        """
405 771 borja
        return self.state_machine.get_state()
406 632 borja
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 771 borja
        self.state_machine.change_state(state)
417 632 borja
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 647 borja
        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 683 borja
        logger.log(loglevel, "Deadline       : %s" % self.deadline)
432 647 borja
        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 691 borja
        logger.log(loglevel, "Price          : %s" % self.price)
436
        logger.log(loglevel, "Extras         : %s" % self.extras)
437 632 borja
        self.print_rrs(loglevel)
438 647 borja
        logger.log(loglevel, "--------------------------------------------------")
439 632 borja
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 647 borja
        """
446
        logger = logging.getLogger("LEASES")
447 632 borja
        if len(self.preparation_rrs) > 0:
448 647 borja
            logger.log(loglevel, "DEPLOYMENT RESOURCE RESERVATIONS")
449
            logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
450 632 borja
            for r in self.preparation_rrs:
451
                r.print_contents(loglevel)
452 647 borja
                logger.log(loglevel, "##")
453
        logger.log(loglevel, "VM RESOURCE RESERVATIONS")
454
        logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~")
455 632 borja
        for r in self.vm_rrs:
456
            r.print_contents(loglevel)
457 647 borja
            logger.log(loglevel, "##")
458 632 borja
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 764 borja
        if len(self.vm_rrs) > 0:
478
            return self.vm_rrs[-1]
479
        else:
480
            return None
481 762 borja
482
    def get_vmrr_at(self, time):
483
        """...
484

485
        """
486
        vmrr_at = None
487
        for vmrr in self.vm_rrs:
488 778 borja
            if time >= vmrr.get_first_start() and time < vmrr.get_final_end():
489 762 borja
                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 778 borja
            if vmrr.get_first_start() > time:
500 762 borja
                vmrr_after.append(vmrr)
501
        return vmrr_after
502 632 borja
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 831 borja
        if vmrr == None:
514
            # Nothing scheduled, no endtime
515
            return None
516
        else:
517
            return vmrr.get_final_end()
518 632 borja
519 764 borja
    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 632 borja
    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 768 borja
        self._update_prematureend()
551 632 borja
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 726 borja
589
        for rr in self.vm_rrs:
590
            rr.clear_rrs()
591 632 borja
        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 829 borja
        time_on_dedicated = self.duration.actual
632 632 borja
        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 741 borja
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 632 borja

645 741 borja
        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 632 borja
    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 741 borja
        self.duration.incr(t)
694 632 borja
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 773 borja
    def sanity_check(self):
721
        prev_time = None
722 774 borja
        prev_vmrr = None
723 773 borja
        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 774 borja
            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 773 borja
            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 741 borja
744 773 borja
            if vmrr.prematureend != None:
745 787 borja
                assert vmrr.prematureend >= vmrr.start and vmrr.prematureend <= vmrr.end
746 773 borja
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 774 borja
752
            prev_vmrr = vmrr
753 773 borja
754 828 borja
        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 774 borja
757 828 borja
758 741 borja
    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 632 borja

765 741 borja
        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 809 borja
        time = 0
773 741 borja
        for vnode in self.requested_resources:
774 809 borja
            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 831 borja
779
    def __repr__(self):
780
        """Returns a string representation of the Lease"""
781
        return "L%i" % self.id
782 741 borja
783 768 borja
    # 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 787 borja
                if known - acc <= rrdur:
791 768 borja
                    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 632 borja
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 647 borja
    """
811 632 borja
    transitions = {Lease.STATE_NEW:                 [(Lease.STATE_PENDING,    "")],
812
813
                   Lease.STATE_PENDING:             [(Lease.STATE_SCHEDULED,  ""),
814
                                                     (Lease.STATE_QUEUED,     ""),
815
                                                     (Lease.STATE_CANCELLED,  ""),
816 715 borja
                                                     (Lease.STATE_REJECTED,   ""),
817
                                                     (Lease.STATE_REJECTED_BY_USER,   "")],
818 632 borja
819
                   Lease.STATE_SCHEDULED:           [(Lease.STATE_PREPARING,  ""),
820
                                                     (Lease.STATE_QUEUED,     ""),
821 647 borja
                                                     (Lease.STATE_PENDING,    ""),
822 632 borja
                                                     (Lease.STATE_READY,      ""),
823 647 borja
                                                     (Lease.STATE_CANCELLED,  ""),
824
                                                     (Lease.STATE_FAIL,       "")],
825 632 borja
826
                   Lease.STATE_QUEUED:              [(Lease.STATE_SCHEDULED,  ""),
827
                                                     (Lease.STATE_CANCELLED,  "")],
828
829
                   Lease.STATE_PREPARING:           [(Lease.STATE_READY,      ""),
830 830 borja
                                                     (Lease.STATE_QUEUED,     ""),
831
                                                     (Lease.STATE_PENDING,    ""),
832 632 borja
                                                     (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 760 borja
                                                     (Lease.STATE_READY,     ""),
843 632 borja
                                                     (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 688 borja
    coupled to the SlotTable class. The Capacity and ResourceTuple
911 632 borja
    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 658 borja
        self.ninstances = dict([(res_type, 1) for res_type in types])
926
        self.quantity = dict([(res_type, [0]) for res_type in types])
927 632 borja
928 658 borja
    def get_ninstances(self, res_type):
929 632 borja
        """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 658 borja
        return self.ninstances[res_type]
936 632 borja
937 658 borja
    def get_quantity(self, res_type):
938 632 borja
        """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 658 borja
        return self.get_quantity_instance(res_type, 1)
945 632 borja
946 658 borja
    def get_quantity_instance(self, res_type, instance):
947 632 borja
        """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 658 borja
        return self.quantity[res_type][instance-1]
957 632 borja
958 658 borja
    def set_quantity(self, res_type, amount):
959 632 borja
        """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 658 borja
        self.set_quantity_instance(res_type, 1, amount)
967 632 borja
968 658 borja
    def set_quantity_instance(self, res_type, instance, amount):
969 632 borja
        """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 658 borja
        self.quantity[res_type][instance-1] = amount
980 632 borja
981 658 borja
    def set_ninstances(self, res_type, ninstances):
982 632 borja
        """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 658 borja
        self.ninstances[res_type] = ninstances
994
        self.quantity[res_type] = [0] * ninstances
995 632 borja
996
    def get_resource_types(self):
997
        """Returns the types of resources in this capacity.
998

999
        """
1000
        return self.quantity.keys()
1001
1002 797 borja
    @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 632 borja
    def __eq__(self, other):
1028
        """Tests if two capacities are the same
1029

1030
        """
1031 658 borja
        for res_type in self.quantity:
1032
            if not other.quantity.has_key(res_type):
1033 632 borja
                return False
1034 658 borja
            if self.ninstances[res_type] != other.ninstances[res_type]:
1035 632 borja
                return False
1036 658 borja
            if self.quantity[res_type] != other.quantity[res_type]:
1037 632 borja
                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 840 borja
        return "  |  ".join("%s: %s" % (res_type,q) for res_type, q in self.quantity.items())
1049 632 borja
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 779 borja
1079
    def is_requested_exact(self):
1080
        return self.requested != Timestamp.UNSPECIFIED and self.requested != Timestamp.NOW
1081 632 borja
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 764 borja
        """
1159 632 borja
        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 658 borja
        SoftwareEnvironment.__init__(self)
1190 632 borja
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 658 borja
        SoftwareEnvironment.__init__(self)
1211 632 borja
1212 658 borja
1213 632 borja
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 683 borja
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 632 borja
1286
            lease = Lease.create_new_from_xml_element(lease)
1287
1288
            realduration = r.find("realduration")
1289
            if realduration != None:
1290 788 borja
                realduration = Parser.DateTimeDeltaFromString(realduration.get("time"))
1291
                if realduration < lease.duration.requested:
1292
                    lease.duration.known = realduration
1293 632 borja
1294
            leases.append(lease)
1295
1296
        return cls(leases)
1297 695 borja
1298
class LeaseAnnotation(object):
1299 797 borja
    """Represents a lease annotation.
1300 695 borja

1301
    ...
1302
    """
1303
    def __init__(self, lease_id, start, deadline, software, extras):
1304
        """Constructor.
1305 632 borja

1306 695 borja
        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 712 borja
                    start = Timestamp(Parser.DateTimeDeltaFromString(child.get("time")))
1348 695 borja
1349
        deadline = element.find("deadline")
1350
1351
        if deadline != None:
1352 712 borja
            deadline = Parser.DateTimeDeltaFromString(deadline.get("time"))
1353 695 borja
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 799 borja
        if self.lease_id != None:
1383
            annotation.set("id", str(self.lease_id))
1384 695 borja
1385 799 borja
        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 695 borja
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 797 borja
    """Represents a sequence of lease annotations.
1427 695 borja

1428
    ...
1429
    """
1430 698 borja
    def __init__(self, annotations, attributes):
1431 695 borja
        """Constructor.
1432

1433
        Arguments:
1434
        annotations -- A dictionary of annotations
1435 799 borja
        """
1436
        if isinstance(annotations, list):
1437
            self.lease_specific_annotations = False
1438
        elif isinstance(annotations, dict):
1439
            self.lease_specific_annotations = True
1440 695 borja
        self.annotations = annotations
1441 698 borja
        self.attributes = attributes
1442 695 borja
1443 799 borja
    def __apply_to_lease(self, lease, annotation):
1444 807 borja
        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 695 borja
1450 807 borja
        if annotation.deadline != None:
1451
            lease.deadline = lease.submit_time + annotation.deadline
1452 695 borja
1453 807 borja
        if annotation.software != None:
1454
            lease.software = annotation.software
1455 695 borja
1456 807 borja
        if annotation.extras != None:
1457
            lease.extras.update(annotation.extras)
1458 799 borja
1459
    def apply_to_leases(self, leases):
1460
        """Apply annotations to a workload
1461 695 borja

1462 799 borja
        """
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 695 borja
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 799 borja
        annotations_dict = {}
1493
        annotations_list = []
1494 695 borja
        for annotation_elem in annotation_elems:
1495 799 borja
            annotation = LeaseAnnotation.from_xml_element(annotation_elem)
1496
            if annotation.lease_id == None:
1497
                annotations_list.append(annotation)
1498
            else:
1499 807 borja
                annotations_dict[int(annotation.lease_id)] = annotation
1500 695 borja
1501 698 borja
        attributes = {}
1502
        attributes_elem = element.find("attributes")
1503 702 borja
        if attributes_elem != None:
1504
            for attr_elem in attributes_elem:
1505
                attributes[attr_elem.get("name")] = attr_elem.get("value")
1506 799 borja
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 807 borja
1514 698 borja
        return cls(annotations, attributes)
1515 695 borja
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 799 borja
        annotations_elem = ET.Element("lease-annotations")
1524 698 borja
1525 799 borja
        attributes = ET.SubElement(annotations_elem, "attributes")
1526 698 borja
        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 799 borja
        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 695 borja
1539 799 borja
        return annotations_elem
1540 695 borja
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 632 borja
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 798 borja
        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 632 borja
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 798 borja
        resources = resource_str[1]
1654 797 borja
        capacity = Capacity.from_resources_string(resources)
1655 632 borja
1656
        nodes = Nodes([(numnodes,capacity)])
1657
1658 797 borja
        return cls(nodes, capacity.get_resource_types(), [])
1659 632 borja
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 798 borja
    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 632 borja
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 658 borja
                res_type = res.get("type")
1755 632 borja
                if len(res.getchildren()) == 0:
1756
                    amount = int(res.get("amount"))
1757 658 borja
                    r.set_ninstances(res_type, 1)
1758
                    r.set_quantity(res_type, amount)
1759 632 borja
                else:
1760
                    instances = res.findall("instance")
1761 795 borja
                    r.set_ninstances(res_type, len(instances))
1762 632 borja
                    for i, instance in enumerate(instances):
1763
                        amount = int(instance.get("amount"))
1764 794 borja
                        r.set_quantity_instance(res_type, i+1, amount)
1765 632 borja
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 658 borja
            for res_type in capacity.get_resource_types():
1784 632 borja
                res = ET.SubElement(nodeset, "res")
1785 658 borja
                res.set("type", res_type)
1786
                ninstances = capacity.get_ninstances(res_type)
1787 632 borja
                if ninstances == 1:
1788 658 borja
                    res.set("amount", str(capacity.get_quantity(res_type)))
1789 632 borja
1790
        return nodes
1791
1792
    def get_all_nodes(self):
1793
        """Returns a dictionary mapping individual nodes to capacities
1794

1795
        """
1796
        nodes = {}
1797
        nodenum = 1
1798
        for node_set in self.node_sets:
1799
            numnodes = node_set[0]
1800
            r = node_set[1]
1801
            for i in range(numnodes):
1802
                nodes[nodenum] = r
1803
                nodenum += 1
1804
        return nodes
1805
1806
    def add_resource(self, name, amounts):
1807
        """Adds a new resource to all the nodes
1808

1809
        Argument:
1810
        name -- Name of the resource type
1811
        amounts -- A list with the amounts of the resource to add to each
1812
        node. If the resource is single-instance, then this will just
1813
        be a list with a single element. If multi-instance, each element
1814
        of the list represent the amount of an instance of the resource.
1815
        """
1816
        for node_set in self.node_sets:
1817
            r = node_set[1]
1818
            r.set_ninstances(name, len(amounts))
1819
            for ninstance, amount in enumerate(amounts):
1820
                r.set_quantity_instance(name, ninstance+1, amount)