Project

General

Profile

root / trunk / src / haizea / core / leases.py @ 675

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
37
from haizea.common.utils import StateMachine, round_datetime_delta, get_lease_id
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
    
89
    # String representation of lease states
90
    state_str = {STATE_NEW : "New",
91
                 STATE_PENDING : "Pending",
92
                 STATE_REJECTED : "Rejected",
93
                 STATE_SCHEDULED : "Scheduled",
94
                 STATE_QUEUED : "Queued",
95
                 STATE_CANCELLED : "Cancelled",
96
                 STATE_PREPARING : "Preparing",
97
                 STATE_READY : "Ready",
98
                 STATE_ACTIVE : "Active",
99
                 STATE_SUSPENDING : "Suspending",
100
                 STATE_SUSPENDED_PENDING : "Suspended-Pending",
101
                 STATE_SUSPENDED_QUEUED : "Suspended-Queued",
102
                 STATE_SUSPENDED_SCHEDULED : "Suspended-Scheduled",
103
                 STATE_MIGRATING : "Migrating",
104
                 STATE_RESUMING : "Resuming",
105
                 STATE_RESUMED_READY: "Resumed-Ready",
106
                 STATE_DONE : "Done",
107
                 STATE_FAIL : "Fail"}
108
    
109
    # Lease types
110
    BEST_EFFORT = 1
111
    ADVANCE_RESERVATION = 2
112
    IMMEDIATE = 3
113
    UNKNOWN = -1
114
    
115
    # String representation of lease types    
116
    type_str = {BEST_EFFORT: "Best-effort",
117
                ADVANCE_RESERVATION: "AR",
118
                IMMEDIATE: "Immediate",
119
                UNKNOWN: "Unknown"}
120
    
121
    def __init__(self, lease_id, submit_time, requested_resources, start, duration, 
122
                 deadline, preemptible, software, state):
123
        """Constructs a lease.
124
        
125
        The arguments are the fundamental attributes of a lease.
126
        The attributes that are not specified by the arguments are
127
        the lease ID (which is an autoincremented integer), the
128
        lease state (a lease always starts out in state "NEW").
129
        A lease also has several bookkeeping attributes that are
130
        only meant to be consumed by other Haizea objects.
131
        
132
        Arguments:
133
        id -- Unique identifier for the lease. If None, one
134
        will be provided.
135
        submit_time -- The time at which the lease was submitted
136
        requested_resources -- A dictionary (int -> Capacity) mapping
137
          each requested node to a capacity (i.e., the amount of
138
          resources requested for that node)
139
        start -- A Timestamp object containing the requested time.
140
        duration -- A Duration object containing the requested duration.
141
        deadline -- A Timestamp object containing the deadline by which
142
          this lease must be completed.
143
        preemptible -- A boolean indicating whether this lease can be
144
          preempted or not.
145
        software -- A SoftwareEnvironment object specifying the
146
          software environment required by the lease.
147
        """        
148
        # Lease ID (read only)
149
        self.id = lease_id
150
        
151
        # Lease attributes
152
        self.submit_time = submit_time
153
        self.requested_resources = requested_resources
154
        self.start = start
155
        self.duration = duration
156
        self.deadline = deadline
157
        self.preemptible = preemptible
158
        self.software = software
159

    
160
        # Bookkeeping attributes:
161

    
162
        # Lease state
163
        if state == None:
164
            state = Lease.STATE_NEW
165
        self.state = LeaseStateMachine(initial_state = state)
166

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

    
185
        # Enactment information. Should only be manipulated by enactment module
186
        self.enactment_info = None
187
        self.vnode_enactment_info = dict([(n, None) for n in self.requested_resources.keys()])
188
        
189
        
190
    @classmethod
191
    def create_new(cls, submit_time, requested_resources, start, duration, 
192
                 deadline, preemptible, software):
193
        lease_id = get_lease_id()
194
        state = Lease.STATE_NEW
195
        return cls(lease_id, submit_time, requested_resources, start, duration, 
196
                 deadline, preemptible, software, state)
197
        
198
    @classmethod
199
    def create_new_from_xml_element(cls, element):
200
        lease = cls.from_xml_element(element)
201
        lease.id = get_lease_id()
202
        lease.state = LeaseStateMachine(initial_state = Lease.STATE_NEW)
203
        return lease
204

    
205
    @classmethod
206
    def from_xml_file(cls, xml_file):
207
        """Constructs a lease from an XML file.
208
        
209
        See the Haizea documentation for details on the
210
        lease XML format.
211
        
212
        Argument:
213
        xml_file -- XML file containing the lease in XML format.
214
        """        
215
        return cls.from_xml_element(ET.parse(xml_file).getroot())
216

    
217
    @classmethod
218
    def from_xml_string(cls, xml_str):
219
        """Constructs a lease from an XML string.
220
        
221
        See the Haizea documentation for details on the
222
        lease XML format.
223
        
224
        Argument:
225
        xml_str -- String containing the lease in XML format.
226
        """        
227
        return cls.from_xml_element(ET.fromstring(xml_str))
228
        
229
    @classmethod
230
    def from_xml_element(cls, element):
231
        """Constructs a lease from an ElementTree element.
232
        
233
        See the Haizea documentation for details on the
234
        lease XML format.
235
        
236
        Argument:
237
        element -- Element object containing a "<lease>" element.
238
        """        
239
        
240
        lease_id = element.get("id")
241
        
242
        if lease_id == None:
243
            lease_id = None
244
        else:
245
            lease_id = int(lease_id)
246

    
247
        state = element.get("state")
248
        if state == None:
249
            state = None
250
        else:
251
            state = int(state)
252

    
253
        
254
        submit_time = element.get("submit-time")
255
        if submit_time == None:
256
            submit_time = None
257
        else:
258
            submit_time = Parser.DateTimeFromString(submit_time)
259
        
260
        nodes = Nodes.from_xml_element(element.find("nodes"))
261
        
262
        requested_resources = nodes.get_all_nodes()
263
        
264
        start = element.find("start")
265
        if len(start.getchildren()) == 0:
266
            start = Timestamp(Timestamp.UNSPECIFIED)
267
        else:
268
            child = start[0]
269
            if child.tag == "now":
270
                start = Timestamp(Timestamp.NOW)
271
            elif child.tag == "exact":
272
                start = Timestamp(Parser.DateTimeFromString(child.get("time")))
273
        
274
        duration = Duration(Parser.DateTimeDeltaFromString(element.find("duration").get("time")))
275

    
276
        deadline = None
277
        
278
        preemptible = element.get("preemptible").capitalize()
279
        if preemptible == "True":
280
            preemptible = True
281
        elif preemptible == "False":
282
            preemptible = False
283
        
284
        software = element.find("software")
285
        
286
        if software.find("none") != None:
287
            software = UnmanagedSoftwareEnvironment()
288
        elif software.find("disk-image") != None:
289
            disk_image = software.find("disk-image")
290
            image_id = disk_image.get("id")
291
            image_size = int(disk_image.get("size"))
292
            software = DiskImageSoftwareEnvironment(image_id, image_size)
293
        
294
        return Lease(lease_id, submit_time, requested_resources, start, duration, 
295
                     deadline, preemptible, software, state)
296

    
297

    
298
    def to_xml(self):
299
        """Returns an ElementTree XML representation of the lease
300
        
301
        See the Haizea documentation for details on the
302
        lease XML format.
303
        
304
        """        
305
        lease = ET.Element("lease")
306
        if self.id != None:
307
            lease.set("id", str(self.id))
308
        lease.set("state", str(self.get_state()))
309
        lease.set("preemptible", str(self.preemptible))
310
        if self.submit_time != None:
311
            lease.set("submit-time", str(self.submit_time))
312
        
313
        capacities = {}
314
        for capacity in self.requested_resources.values():
315
            key = capacity
316
            for c in capacities:
317
                if capacity == c:
318
                    key = c
319
                    break
320
            numnodes = capacities.setdefault(key, 0)
321
            capacities[key] += 1
322
        
323
        nodes = Nodes([(numnodes,c) for c,numnodes in capacities.items()])
324
        lease.append(nodes.to_xml())
325
        
326
        start = ET.SubElement(lease, "start")
327
        if self.start.requested == Timestamp.UNSPECIFIED:
328
            pass # empty start element
329
        elif self.start.requested == Timestamp.NOW:
330
            ET.SubElement(start, "now") #empty now element
331
        else:
332
            exact = ET.SubElement(start, "exact")
333
            exact.set("time", str(self.start.requested))
334
            
335
        duration = ET.SubElement(lease, "duration")
336
        duration.set("time", str(self.duration.requested))
337
        
338
        software = ET.SubElement(lease, "software")
339
        if isinstance(self.software, UnmanagedSoftwareEnvironment):
340
            ET.SubElement(software, "none")
341
        elif isinstance(self.software, DiskImageSoftwareEnvironment):
342
            imagetransfer = ET.SubElement(software, "disk-image")
343
            imagetransfer.set("id", self.software.image_id)
344
            imagetransfer.set("size", str(self.software.image_size))
345
            
346
        return lease
347

    
348
    def to_xml_string(self):
349
        """Returns a string XML representation of the lease
350
        
351
        See the Haizea documentation for details on the
352
        lease XML format.
353
        
354
        """   
355
        return ET.tostring(self.to_xml())
356

    
357
    def get_type(self):
358
        """Determines the type of lease
359
        
360
        Based on the lease's attributes, determines the lease's type.
361
        Can return Lease.BEST_EFFORT, Lease.ADVANCE_RESERVATION, or
362
        Lease.IMMEDIATE
363
        
364
        """
365
        if self.start.requested == Timestamp.UNSPECIFIED:
366
            return Lease.BEST_EFFORT
367
        elif self.start.requested == Timestamp.NOW:
368
            return Lease.IMMEDIATE            
369
        else:
370
            return Lease.ADVANCE_RESERVATION
371
        
372
    def get_state(self):
373
        """Returns the lease's state.
374
                
375
        """        
376
        return self.state.get_state()
377
    
378
    def set_state(self, state):
379
        """Changes the lease's state.
380
                
381
        The state machine will throw an exception if the 
382
        requested transition is illegal.
383
        
384
        Argument:
385
        state -- The new state
386
        """        
387
        self.state.change_state(state)
388
        
389
    def print_contents(self, loglevel=LOGLEVEL_VDEBUG):
390
        """Prints the lease's attributes to the log.
391
                
392
        Argument:
393
        loglevel -- The loglevel at which to print the information
394
        """           
395
        logger = logging.getLogger("LEASES")
396
        logger.log(loglevel, "__________________________________________________")
397
        logger.log(loglevel, "Lease ID       : %i" % self.id)
398
        logger.log(loglevel, "Type           : %s" % Lease.type_str[self.get_type()])
399
        logger.log(loglevel, "Submission time: %s" % self.submit_time)
400
        logger.log(loglevel, "Start          : %s" % self.start)
401
        logger.log(loglevel, "Duration       : %s" % self.duration)
402
        logger.log(loglevel, "State          : %s" % Lease.state_str[self.get_state()])
403
        logger.log(loglevel, "Resource req   : %s" % self.requested_resources)
404
        logger.log(loglevel, "Software       : %s" % self.software)
405
        self.print_rrs(loglevel)
406
        logger.log(loglevel, "--------------------------------------------------")
407

    
408
    def print_rrs(self, loglevel=LOGLEVEL_VDEBUG):
409
        """Prints the lease's resource reservations to the log.
410
                
411
        Argument:
412
        loglevel -- The loglevel at which to print the information
413
        """            
414
        logger = logging.getLogger("LEASES")  
415
        if len(self.preparation_rrs) > 0:
416
            logger.log(loglevel, "DEPLOYMENT RESOURCE RESERVATIONS")
417
            logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
418
            for r in self.preparation_rrs:
419
                r.print_contents(loglevel)
420
                logger.log(loglevel, "##")
421
        logger.log(loglevel, "VM RESOURCE RESERVATIONS")
422
        logger.log(loglevel, "~~~~~~~~~~~~~~~~~~~~~~~~")
423
        for r in self.vm_rrs:
424
            r.print_contents(loglevel)
425
            logger.log(loglevel, "##")
426

    
427
    def get_active_vmrrs(self, time):
428
        """Returns the active VM resource reservations at a given time
429
                
430
        Argument:
431
        time -- Time to look for active reservations
432
        """        
433
        return [r for r in self.vm_rrs if r.start <= time and time <= r.end and r.state == ResourceReservation.STATE_ACTIVE]
434

    
435
    def get_scheduled_reservations(self):
436
        """Returns all scheduled reservations
437
                
438
        """           
439
        return [r for r in self.preparation_rrs + self.vm_rrs if r.state == ResourceReservation.STATE_SCHEDULED]
440

    
441
    def get_last_vmrr(self):
442
        """Returns the last VM reservation for this lease.
443
                        
444
        """            
445
        return self.vm_rrs[-1]    
446

    
447
    def get_endtime(self):
448
        """Returns the time at which the last VM reservation 
449
        for this lease ends.
450
        
451
        Note that this is not necessarily the time at which the lease
452
        will end, just the time at which the last currently scheduled
453
        VM will end.
454
                
455
        """        
456
        vmrr = self.get_last_vmrr()
457
        return vmrr.end
458
    
459
    def append_vmrr(self, vmrr):
460
        """Adds a VM resource reservation to the lease.
461
        
462
        Argument:
463
        vmrr -- The VM RR to add.
464
        """             
465
        self.vm_rrs.append(vmrr)
466
        
467
    def remove_vmrr(self, vmrr):
468
        """Removes a VM resource reservation from the lease.
469
        
470
        Argument:
471
        vmrr -- The VM RR to remove.
472
        """           
473
        if not vmrr in self.vm_rrs:
474
            raise Exception, "Tried to remove an VM RR not contained in this lease"
475
        else:
476
            self.vm_rrs.remove(vmrr)
477
                    
478
    def append_preparationrr(self, preparation_rr):
479
        """Adds a preparation resource reservation to the lease.
480
        
481
        Argument:
482
        preparation_rr -- The preparation RR to add.
483
        """             
484
        self.preparation_rrs.append(preparation_rr)
485
        
486
    def remove_preparationrr(self, preparation_rr):
487
        """Removes a preparation resource reservation from the lease.
488
        
489
        Argument:
490
        preparation_rr -- The preparation RR to remove.
491
        """        
492
        if not preparation_rr in self.preparation_rrs:
493
            raise Exception, "Tried to remove a preparation RR not contained in this lease"
494
        else:
495
            self.preparation_rrs.remove(preparation_rr)        
496

    
497
    def clear_rrs(self):
498
        """Removes all resource reservations for this lease
499
        (both preparation and VM)
500
        
501
        """            
502
        self.preparation_rrs = []
503
        self.vm_rrs = []
504

    
505
    def get_waiting_time(self):
506
        """Gets the waiting time for this lease.
507
        
508
        The waiting time is the difference between the submission
509
        time and the time at which the lease start. This method
510
        mostly makes sense for best-effort leases, where the
511
        starting time is determined by Haizea.
512
        
513
        """          
514
        return self.start.actual - self.submit_time
515
        
516
    def get_slowdown(self, bound=10):
517
        """Determines the bounded slowdown for this lease.
518
        
519
        Slowdown is a normalized measure of how much time a
520
        request takes to make it through a queue (thus, like
521
        get_waiting_time, the slowdown makes sense mostly for
522
        best-effort leases). Slowdown is equal to the time the
523
        lease took to run on a loaded system (i.e., a system where
524
        it had to compete with other leases for resources)
525
        divided by the time it would take if it just had the
526
        system all to itself (i.e., starts running immediately
527
        without having to wait in a queue and without the
528
        possibility of being preempted).
529
        
530
        "Bounded" slowdown is one where leases with very short
531
        durations are rounded up to a bound, to prevent the
532
        metric to be affected by reasonable but disproportionate
533
        waiting times (e.g., a 5-second lease with a 15 second
534
        waiting time -an arguably reasonable waiting time- has a 
535
        slowdown of 4, the same as 10 hour lease having to wait 
536
        30 hours for resources).
537
        
538
        Argument:
539
        bound -- The bound, specified in seconds.
540
        All leases with a duration less than this
541
        parameter are rounded up to the bound.
542
        """          
543
        time_on_dedicated = self.duration.original
544
        time_on_loaded = self.end - self.submit_time
545
        bound = TimeDelta(seconds=bound)
546
        if time_on_dedicated < bound:
547
            time_on_dedicated = bound
548
        return time_on_loaded / time_on_dedicated
549
        
550
    def add_boot_overhead(self, t):
551
        """Adds a boot overhead to the lease.
552
        
553
        Increments the requested duration to account for the fact 
554
        that some time will be spent booting up the resources.
555
        
556
        Argument:
557
        t -- Time to add
558
        """          
559
        self.duration.incr(t)        
560

    
561
    def add_runtime_overhead(self, percent):
562
        """Adds a runtime overhead to the lease.
563
        
564
        This method is mostly meant for simulations. Since VMs
565
        run slower than physical hardware, this increments the
566
        duration of a lease by a percent to observe the effect
567
        of having all the leases run slower on account of
568
        running on a VM.
569
        
570
        Note: the whole "runtime overhead" problem is becoming
571
        increasingly moot as people have lost their aversion to
572
        VMs thanks to the cloud computing craze. Anecdotal evidence
573
        suggests that most people don't care that VMs will run
574
        X % slower (compared to a physical machine) because they
575
        know full well that what they're getting is a virtual
576
        machine (the same way a user of an HPC system would know
577
        that he/she's getting processors with speed X as opposed to
578
        those on some other site, with speed X*0.10)
579
        
580
        Argument:
581
        percent -- Runtime overhead (in percent of requested
582
        duration) to add to the lease.
583
        """            
584
        self.duration.incr_by_percent(percent)
585
            
586
        
587
class LeaseStateMachine(StateMachine):
588
    """A lease state machine
589
    
590
    A child of StateMachine, this class simply specifies the valid
591
    states and transitions for a lease (the actual state machine code
592
    is in StateMachine).
593
    
594
    See the Haizea documentation for a description of states and
595
    valid transitions.
596
    
597
    """
598
    transitions = {Lease.STATE_NEW:                 [(Lease.STATE_PENDING,    "")],
599
                   
600
                   Lease.STATE_PENDING:             [(Lease.STATE_SCHEDULED,  ""),
601
                                                     (Lease.STATE_QUEUED,     ""),
602
                                                     (Lease.STATE_CANCELLED,  ""),
603
                                                     (Lease.STATE_REJECTED,   "")],
604
                                                     
605
                   Lease.STATE_SCHEDULED:           [(Lease.STATE_PREPARING,  ""),
606
                                                     (Lease.STATE_QUEUED,     ""),
607
                                                     (Lease.STATE_PENDING,    ""),
608
                                                     (Lease.STATE_READY,      ""),
609
                                                     (Lease.STATE_CANCELLED,  ""),
610
                                                     (Lease.STATE_FAIL,       "")],
611
                                                     
612
                   Lease.STATE_QUEUED:              [(Lease.STATE_SCHEDULED,  ""),
613
                                                     (Lease.STATE_CANCELLED,  "")],
614
                                                     
615
                   Lease.STATE_PREPARING:           [(Lease.STATE_READY,      ""),
616
                                                     (Lease.STATE_PENDING,     ""),
617
                                                     (Lease.STATE_CANCELLED,  ""),
618
                                                     (Lease.STATE_FAIL,       "")],
619
                                                     
620
                   Lease.STATE_READY:               [(Lease.STATE_ACTIVE,     ""),
621
                                                     (Lease.STATE_QUEUED,     ""),
622
                                                     (Lease.STATE_PENDING,     ""),
623
                                                     (Lease.STATE_CANCELLED,  ""),
624
                                                     (Lease.STATE_FAIL,       "")],
625
                                                     
626
                   Lease.STATE_ACTIVE:              [(Lease.STATE_SUSPENDING, ""),
627
                                                     (Lease.STATE_QUEUED,     ""),
628
                                                     (Lease.STATE_DONE,       ""),
629
                                                     (Lease.STATE_CANCELLED,  ""),
630
                                                     (Lease.STATE_FAIL,       "")],
631
                                                     
632
                   Lease.STATE_SUSPENDING:          [(Lease.STATE_SUSPENDED_PENDING,  ""),
633
                                                     (Lease.STATE_CANCELLED,  ""),
634
                                                     (Lease.STATE_FAIL,       "")],
635
                                                     
636
                   Lease.STATE_SUSPENDED_PENDING:   [(Lease.STATE_SUSPENDED_QUEUED,     ""),
637
                                                     (Lease.STATE_SUSPENDED_SCHEDULED,  ""),
638
                                                     (Lease.STATE_CANCELLED,  ""),
639
                                                     (Lease.STATE_FAIL,       "")],
640
                                                     
641
                   Lease.STATE_SUSPENDED_QUEUED:    [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
642
                                                     (Lease.STATE_CANCELLED,  ""),
643
                                                     (Lease.STATE_FAIL,       "")],
644
                                                     
645
                   Lease.STATE_SUSPENDED_SCHEDULED: [(Lease.STATE_SUSPENDED_QUEUED,     ""),
646
                                                     (Lease.STATE_SUSPENDED_PENDING,  ""),
647
                                                     (Lease.STATE_MIGRATING,  ""),
648
                                                     (Lease.STATE_RESUMING,   ""),
649
                                                     (Lease.STATE_CANCELLED,  ""),
650
                                                     (Lease.STATE_FAIL,       "")],
651
                                                     
652
                   Lease.STATE_MIGRATING:           [(Lease.STATE_SUSPENDED_SCHEDULED,  ""),
653
                                                     (Lease.STATE_CANCELLED,  ""),
654
                                                     (Lease.STATE_FAIL,       "")],
655
                                                     
656
                   Lease.STATE_RESUMING:            [(Lease.STATE_RESUMED_READY, ""),
657
                                                     (Lease.STATE_CANCELLED,  ""),
658
                                                     (Lease.STATE_FAIL,       "")],
659
                                                     
660
                   Lease.STATE_RESUMED_READY:       [(Lease.STATE_ACTIVE,     ""),
661
                                                     (Lease.STATE_CANCELLED,  ""),
662
                                                     (Lease.STATE_FAIL,       "")],
663
                   
664
                   # Final states
665
                   Lease.STATE_DONE:          [],
666
                   Lease.STATE_CANCELLED:     [],
667
                   Lease.STATE_FAIL:          [],
668
                   Lease.STATE_REJECTED:      [],
669
                   }
670
    
671
    def __init__(self, initial_state):
672
        StateMachine.__init__(self, initial_state, LeaseStateMachine.transitions, Lease.state_str)
673

    
674

    
675
class Capacity(object):
676
    """A quantity of resources
677
    
678
    This class is used to represent a quantity of resources, such
679
    as those required by a lease. For example, if a lease needs a
680
    single node with 1 CPU and 1024 MB of memory, a single Capacity
681
    object would be used containing that information. 
682
    
683
    Resources in a Capacity object can be multi-instance, meaning
684
    that several instances of the same type of resources can be
685
    specified. For example, if a node requires 2 CPUs, then this is
686
    represented as two instances of the same type of resource. Most
687
    resources, however, will be "single instance" (e.g., a physical
688
    node only has "one" memory).
689
    
690
    Note: This class is similar, but distinct from, the ResourceTuple
691
    class in the slottable module. The ResourceTuple class can contain
692
    the same information, but uses a different internal representation
693
    (which is optimized for long-running simulations) and is tightly
694
    coupled to the SlotTable class. The Quantity and ResourceTuple
695
    classes are kept separate so that the slottable module remains
696
    independent from the rest of Haizea (in case we want to switch
697
    to a different slottable implementation in the future).
698
    
699
    """        
700
    def __init__(self, types):
701
        """Constructs an empty Capacity object.
702
        
703
        All resource types are initially set to be single-instance,
704
        with a quantity of 0 for each resource.
705
        
706
        Argument:
707
        types -- List of resource types. e.g., ["CPU", "Memory"]
708
        """          
709
        self.ninstances = dict([(res_type, 1) for res_type in types])
710
        self.quantity = dict([(res_type, [0]) for res_type in types])
711
        
712
    def get_ninstances(self, res_type):
713
        """Gets the number of instances for a resource type
714
                
715
        Argument:
716
        type -- The type of resource (using the same name passed
717
        when constructing the Capacity object)
718
        """               
719
        return self.ninstances[res_type]
720
           
721
    def get_quantity(self, res_type):
722
        """Gets the quantity of a single-instance resource
723
                
724
        Argument:
725
        type -- The type of resource (using the same name passed
726
        when constructing the Capacity object)
727
        """               
728
        return self.get_quantity_instance(res_type, 1)
729
    
730
    def get_quantity_instance(self, res_type, instance):
731
        """Gets the quantity of a specific instance of a 
732
        multi-instance resource.
733
                        
734
        Argument:
735
        type -- The type of resource (using the same name passed
736
        when constructing the Capacity object)
737
        instance -- The instance. Note that instances are numbered
738
        from 1.
739
        """               
740
        return self.quantity[res_type][instance-1]
741

    
742
    def set_quantity(self, res_type, amount):
743
        """Sets the quantity of a single-instance resource
744
                
745
        Argument:
746
        type -- The type of resource (using the same name passed
747
        when constructing the Capacity object)
748
        amount -- The amount to set the resource to.
749
        """            
750
        self.set_quantity_instance(res_type, 1, amount)
751
    
752
    def set_quantity_instance(self, res_type, instance, amount):
753
        """Sets the quantity of a specific instance of a 
754
        multi-instance resource.
755
                        
756
        Argument:
757
        type -- The type of resource (using the same name passed
758
        when constructing the Capacity object)
759
        instance -- The instance. Note that instances are numbered
760
        from 1.
761
        amount -- The amount to set the instance of the resource to.
762
        """        
763
        self.quantity[res_type][instance-1] = amount
764
    
765
    def set_ninstances(self, res_type, ninstances):
766
        """Changes the number of instances of a resource type.
767
                        
768
        Note that changing the number of instances will initialize
769
        all the instances' amounts to zero. This method should
770
        only be called right after constructing a Capacity object.
771
        
772
        Argument:
773
        type -- The type of resource (using the same name passed
774
        when constructing the Capacity object)
775
        ninstance -- The number of instances
776
        """                
777
        self.ninstances[res_type] = ninstances
778
        self.quantity[res_type] = [0] * ninstances
779
       
780
    def get_resource_types(self):
781
        """Returns the types of resources in this capacity.
782
                        
783
        """            
784
        return self.quantity.keys()
785
    
786
    def __eq__(self, other):
787
        """Tests if two capacities are the same
788
                        
789
        """        
790
        for res_type in self.quantity:
791
            if not other.quantity.has_key(res_type):
792
                return False
793
            if self.ninstances[res_type] != other.ninstances[res_type]:
794
                return False
795
            if self.quantity[res_type] != other.quantity[res_type]:
796
                return False
797
        return True
798

    
799
    def __ne__(self, other):
800
        """Tests if two capacities are not the same
801
                        
802
        """        
803
        return not self == other
804
            
805
    def __repr__(self):
806
        """Returns a string representation of the Capacity"""
807
        return "  |  ".join("%s: %i" % (res_type,q[0]) for res_type, q in self.quantity.items())
808
            
809

    
810
class Timestamp(object):
811
    """An exact point in time.
812
    
813
    This class is just a wrapper around three DateTimes. When
814
    dealing with timestamps in Haizea (such as the requested
815
    starting time for a lease), we want to keep track not just
816
    of the requested timestamp, but also the scheduled timestamp
817
    (which could differ from the requested one) and the
818
    actual timestamp (which could differ from the scheduled one).
819
    """        
820
    
821
    UNSPECIFIED = "Unspecified"
822
    NOW = "Now"
823
    
824
    def __init__(self, requested):
825
        """Constructor
826
                        
827
        Argument:
828
        requested -- The requested timestamp
829
        """        
830
        self.requested = requested
831
        self.scheduled = None
832
        self.actual = None
833

    
834
    def __repr__(self):
835
        """Returns a string representation of the Duration"""
836
        return "REQ: %s  |  SCH: %s  |  ACT: %s" % (self.requested, self.scheduled, self.actual)
837
        
838
class Duration(object):
839
    """A duration
840
    
841
    This class is just a wrapper around five DateTimes. When
842
    dealing with durations in Haizea (such as the requested
843
    duration for a lease), we want to keep track of the following:
844
    
845
    - The requested duration
846
    - The accumulated duration (when the entire duration of
847
    the lease can't be scheduled without interrumption, this
848
    keeps track of how much duration has been fulfilled so far)
849
    - The actual duration (which might not be the same as the
850
    requested duration)
851
    
852
    For the purposes of simulation, we also want to keep track
853
    of the "original" duration (since the requested duration
854
    can be modified to simulate certain overheads) and the
855
    "known" duration (when simulating lease workloads, this is
856
    the actual duration of the lease, which is known a posteriori).
857
    """  
858
    
859
    def __init__(self, requested, known=None):
860
        """Constructor
861
                        
862
        Argument:
863
        requested -- The requested duration
864
        known -- The known duration (ONLY in simulation)
865
        """              
866
        self.original = requested
867
        self.requested = requested
868
        self.accumulated = TimeDelta()
869
        self.actual = None
870
        # The following is ONLY used in simulation
871
        self.known = known
872
        
873
    def incr(self, t):
874
        """Increments the requested duration by an amount.
875
                        
876
        Argument:
877
        t -- The time to add to the requested duration.
878
        """               
879
        self.requested += t
880
        if self.known != None:
881
            self.known += t
882
            
883
    def incr_by_percent(self, pct):
884
        """Increments the requested duration by a percentage.
885
                        
886
        Argument:
887
        pct -- The percentage of the requested duration to add.
888
        """          
889
        factor = 1 + float(pct)/100
890
        self.requested = round_datetime_delta(self.requested * factor)
891
        if self.known != None:
892
            self.requested = round_datetime_delta(self.known * factor)
893
        
894
    def accumulate_duration(self, t):
895
        """Increments the accumulated duration by an amount.
896
                        
897
        Argument:
898
        t -- The time to add to the accumulated duration.
899
        """        
900
        self.accumulated += t
901
            
902
    def get_remaining_duration(self):
903
        """Returns the amount of time required to fulfil the entire
904
        requested duration of the lease.
905
                        
906
        """         
907
        return self.requested - self.accumulated
908

    
909
    def get_remaining_known_duration(self):
910
        """Returns the amount of time required to fulfil the entire
911
        known duration of the lease.
912
              
913
        ONLY for simulations.
914
        """           
915
        return self.known - self.accumulated
916
            
917
    def __repr__(self):
918
        """Returns a string representation of the Duration"""
919
        return "REQ: %s  |  ACC: %s  |  ACT: %s  |  KNW: %s" % (self.requested, self.accumulated, self.actual, self.known)
920
    
921
class SoftwareEnvironment(object):
922
    """The base class for a lease's software environment"""
923
    
924
    def __init__(self):
925
        """Constructor.
926
        
927
        Does nothing."""
928
        pass
929

    
930
class UnmanagedSoftwareEnvironment(SoftwareEnvironment):
931
    """Represents an "unmanaged" software environment.
932
    
933
    When a lease has an unmanaged software environment,
934
    Haizea does not need to perform any actions to prepare
935
    a lease's software environment (it assumes that this
936
    task is carried out by an external entity, and that
937
    software environments can be assumed to be ready
938
    when a lease has to start; e.g., if VM disk images are
939
    predeployed on all physical nodes)."""
940
    
941
    def __init__(self):
942
        """Constructor.
943
        
944
        Does nothing."""        
945
        SoftwareEnvironment.__init__(self)
946

    
947
class DiskImageSoftwareEnvironment(SoftwareEnvironment):
948
    """Reprents a software environment encapsulated in a disk image.
949
    
950
    When a lease's software environment is contained in a disk image,
951
    this disk image must be deployed to the physical nodes the lease
952
    is mapped to before the lease can start. This means that the
953
    preparation for this lease must be handled by a preparation
954
    scheduler (see documentation in lease_scheduler) capable of
955
    handling a DiskImageSoftwareEnvironment.
956
    """
957
    def __init__(self, image_id, image_size):
958
        """Constructor.
959
        
960
        Arguments:
961
        image_id -- A unique identifier for the disk image required
962
        by the lease.
963
        image_size -- The size, in MB, of the disk image. """         
964
        self.image_id = image_id
965
        self.image_size = image_size
966
        SoftwareEnvironment.__init__(self)
967

    
968

    
969
    
970
class LeaseWorkload(object):
971
    """Reprents a sequence of lease requests.
972
    
973
    A lease workload is a sequence of lease requests with a specific
974
    arrival time for each lease. This class is currently only used
975
    to load LWF (Lease Workload File) files. See the Haizea documentation 
976
    for details on the LWF format.
977
    """    
978
    def __init__(self, leases):
979
        """Constructor.
980
        
981
        Arguments:
982
        leases -- An ordered list (by arrival time) of leases in the workload
983
        """                 
984
        self.leases = leases
985
        
986

    
987
    def get_leases(self):
988
        """Returns the leases in the workload.
989
        
990
        """  
991
        return self.leases
992
    
993
    @classmethod
994
    def from_xml_file(cls, xml_file, inittime = DateTime(0)):
995
        """Constructs a lease workload from an XML file.
996
        
997
        See the Haizea documentation for details on the
998
        lease workload XML format.
999
        
1000
        Argument:
1001
        xml_file -- XML file containing the lease in XML format.
1002
        inittime -- The starting time of the lease workload. All relative
1003
        times in the XML file will be converted to absolute times by
1004
        adding them to inittime. If inittime is not specified, it will
1005
        arbitrarily be 0000/01/01 00:00:00.
1006
        """        
1007
        return cls.__from_xml_element(ET.parse(xml_file).getroot(), inittime)
1008

    
1009
    # TODO: need to adapt the old SWF trace reading code to new Lease
1010
    # data structures
1011
#    @classmethod
1012
#    def from_swf_file(cls, swf_file, inittime = DateTime(0)):
1013
#        file = open (tracefile, "r")
1014
#        requests = []
1015
#        inittime = config.get("starttime")
1016
#        for line in file:
1017
#            if line[0]!=';':
1018
#                req = None
1019
#                fields = line.split()
1020
#                reqtime = float(fields[8])
1021
#                runtime = int(fields[3]) # 3: RunTime
1022
#                waittime = int(fields[2])
1023
#                status = int(fields[10])
1024
#                
1025
#                if reqtime > 0:
1026
#                    tSubmit = int(fields[1]) # 1: Submission time
1027
#                    tSubmit = inittime + TimeDelta(seconds=tSubmit) 
1028
#                    vmimage = "NOIMAGE"
1029
#                    vmimagesize = 600 # Arbitrary
1030
#                    numnodes = int(fields[7]) # 7: reqNProcs
1031
#                    resreq = ResourceTuple.create_empty()
1032
#                    resreq.set_by_type(constants.RES_CPU, 1) # One CPU per VM, should be configurable
1033
#                    resreq.set_by_type(constants.RES_MEM, 1024) # Should be configurable
1034
#                    resreq.set_by_type(constants.RES_DISK, vmimagesize + 0) # Should be configurable
1035
#                    maxdur = TimeDelta(seconds=reqtime)
1036
#                    if runtime < 0 and status==5:
1037
#                        # This is a job that got cancelled while waiting in the queue
1038
#                        continue
1039
#                    else:
1040
#                        if runtime == 0:
1041
#                            runtime = 1 # Runtime of 0 is <0.5 rounded down.
1042
#                        realdur = TimeDelta(seconds=runtime) # 3: RunTime
1043
#                    if realdur > maxdur:
1044
#                        realdur = maxdur
1045
#                    preemptible = True
1046
#                    req = BestEffortLease(tSubmit, maxdur, vmimage, vmimagesize, numnodes, resreq, preemptible, realdur)
1047
#                    requests.append(req)
1048
#        return requests
1049

    
1050
    @classmethod
1051
    def __from_xml_element(cls, element, inittime):
1052
        """Constructs a lease from an ElementTree element.
1053
        
1054
        See the Haizea documentation for details on the
1055
        lease XML format.
1056
        
1057
        Argument:
1058
        element -- Element object containing a "<lease-workload>" element.
1059
        inittime -- The starting time of the lease workload. All relative
1060
        times in the XML file will be converted to absolute times by
1061
        adding them to inittime.  
1062
        """                
1063
        reqs = element.findall("lease-requests/lease-request")
1064
        leases = []
1065
        for r in reqs:
1066
            lease = r.find("lease")
1067
            # Add time lease is submitted
1068
            submittime = inittime + Parser.DateTimeDeltaFromString(r.get("arrival"))
1069
            lease.set("submit-time", str(submittime))
1070
            
1071
            # If an exact starting time is specified, add the init time
1072
            exact = lease.find("start/exact")
1073
            if exact != None:
1074
                start = inittime + Parser.DateTimeDeltaFromString(exact.get("time"))
1075
                exact.set("time", str(start))
1076
                
1077
            lease = Lease.create_new_from_xml_element(lease)
1078
            
1079
            realduration = r.find("realduration")
1080
            if realduration != None:
1081
                lease.duration.known = Parser.DateTimeDeltaFromString(realduration.get("time"))
1082

    
1083
            leases.append(lease)
1084
            
1085
        return cls(leases)
1086
        
1087
class Site(object):
1088
    """Represents a site containing machines ("nodes").
1089
    
1090
    This class is used to load site descriptions in XML format or
1091
    using a "resources string". Site descriptions can appear in two places:
1092
    in a LWF file (where the site required for the lease workload is
1093
    embedded in the LWF file) or in the Haizea configuration file. In both
1094
    cases, the site description is only used in simulation (in OpenNebula mode,
1095
    the available nodes and resources are obtained by querying OpenNebula). 
1096
    
1097
    Note that this class is distinct from the ResourcePool class, even though
1098
    both are used to represent "collections of nodes". The Site class is used
1099
    purely as a convenient way to load site information from an XML file
1100
    and to manipulate that information elsewhere in Haizea, while the
1101
    ResourcePool class is responsible for sending enactment commands
1102
    to nodes, monitoring nodes, etc.
1103
    """        
1104
    def __init__(self, nodes, resource_types, attr_types):
1105
        """Constructor.
1106
        
1107
        Arguments:
1108
        nodes -- A Nodes object
1109
        resource_types -- A list of valid resource types in this site.
1110
        attr_types -- A list of valid attribute types in this site
1111
        """             
1112
        self.nodes = nodes
1113
        self.resource_types = resource_types
1114
        self.attr_types = attr_types
1115
        
1116
    @classmethod
1117
    def from_xml_file(cls, xml_file):
1118
        """Constructs a site from an XML file.
1119
        
1120
        See the Haizea documentation for details on the
1121
        site XML format.
1122
        
1123
        Argument:
1124
        xml_file -- XML file containing the site in XML format.
1125
        """                
1126
        return cls.__from_xml_element(ET.parse(xml_file).getroot())        
1127

    
1128
    @classmethod
1129
    def from_lwf_file(cls, lwf_file):
1130
        """Constructs a site from an LWF file.
1131
        
1132
        LWF files can have site information embedded in them. This method
1133
        loads this site information from an LWF file. See the Haizea 
1134
        documentation for details on the LWF format.
1135
        
1136
        Argument:
1137
        lwf_file -- LWF file.
1138
        """                
1139
        return cls.__from_xml_element(ET.parse(lwf_file).getroot().find("site"))        
1140
        
1141
    @classmethod
1142
    def __from_xml_element(cls, element):     
1143
        """Constructs a site from an ElementTree element.
1144
        
1145
        See the Haizea documentation for details on the
1146
        site XML format.
1147
        
1148
        Argument:
1149
        element -- Element object containing a "<site>" element.
1150
        """     
1151
        resource_types = element.find("resource-types")
1152
        resource_types = resource_types.get("names").split()
1153
       
1154
        # TODO: Attributes
1155
        attrs = []
1156
        
1157
        nodes = Nodes.from_xml_element(element.find("nodes"))
1158

    
1159
        # Validate nodes
1160
        for node_set in nodes.node_sets:
1161
            capacity = node_set[1]
1162
            for resource_type in capacity.get_resource_types():
1163
                if resource_type not in resource_types:
1164
                    # TODO: Raise something more meaningful
1165
                    raise Exception
1166

    
1167
        return cls(nodes, resource_types, attrs)
1168
    
1169
    @classmethod
1170
    def from_resources_string(cls, resource_str):
1171
        """Constructs a site from a "resources string"
1172
        
1173
        A "resources string" is a shorthand way of specifying a site
1174
        with homogeneous resources and no attributes. The format is:
1175
        
1176
        <numnodes> <resource_type>:<resource_quantity>[,<resource_type>:<resource_quantity>]*
1177
        
1178
        For example: 4 CPU:100,Memory:1024
1179
        
1180
        Argument:
1181
        resource_str -- resources string
1182
        """    
1183

    
1184
        resource_str = resource_str.split()
1185
        numnodes = int(resource_str[0])
1186
        resources = resource_str[1:]
1187
        res = {}
1188
        
1189
        for r in resources:
1190
            res_type, amount = r.split(":")
1191
            res[res_type] = int(amount)
1192
            
1193
        capacity = Capacity(res.keys())
1194
        for (res_type, amount) in res.items():
1195
            capacity.set_quantity(res_type, amount)
1196
        
1197
        nodes = Nodes([(numnodes,capacity)])
1198

    
1199
        return cls(nodes, res.keys(), [])
1200
            
1201
    def add_resource(self, name, amounts):
1202
        """Adds a new resource to all nodes in the site.
1203
                
1204
        Argument:
1205
        name -- Name of the resource type
1206
        amounts -- A list with the amounts of the resource to add to each
1207
        node. If the resource is single-instance, then this will just
1208
        be a list with a single element. If multi-instance, each element
1209
        of the list represent the amount of an instance of the resource.
1210
        """            
1211
        self.resource_types.append(name)
1212
        self.nodes.add_resource(name, amounts)
1213
    
1214
    def get_resource_types(self):
1215
        """Returns the resource types in this site.
1216
        
1217
        This method returns a list, each item being a pair with
1218
        1. the name of the resource type and 2. the maximum number of
1219
        instances for that resource type across all nodes.
1220
                
1221
        """               
1222
        max_ninstances = dict((rt, 1) for rt in self.resource_types)
1223
        for node_set in self.nodes.node_sets:
1224
            capacity = node_set[1]
1225
            for resource_type in capacity.get_resource_types():
1226
                if capacity.ninstances[resource_type] > max_ninstances[resource_type]:
1227
                    max_ninstances[resource_type] = capacity.ninstances[resource_type]
1228
                    
1229
        max_ninstances = [(rt,max_ninstances[rt]) for rt in self.resource_types]
1230

    
1231
        return max_ninstances
1232
    
1233

    
1234

    
1235
class Nodes(object):
1236
    """Represents a collection of machines ("nodes")
1237
    
1238
    This class is used to load descriptions of nodes from an XML
1239
    file. These nodes can appear in two places: in a site description
1240
    (which, in turn, is loaded by the Site class) or in a lease's
1241
    resource requirements (describing what nodes, with what resources,
1242
    are required by the lease).
1243
    
1244
    Nodes are stored as one or more "node sets". Each node set has nodes
1245
    with the exact same resources. So, for example, a lease requiring 100
1246
    nodes (all identical, except 50 have 1024MB of memory and the other 50
1247
    have 512MB of memory) doesn't need to enumerate all 100 nodes. Instead,
1248
    it just has to describe the two "node sets" (indicating that there are
1249
    50 nodes of one type and 50 of the other). See the Haizea documentation
1250
    for more details on the XML format.
1251
    
1252
    Like the Site class, this class is distinct from the ResourcePool class, even
1253
    though they both represent a "collection of nodes". See the 
1254
    Site class documentation for more details.
1255
    """            
1256
    def __init__(self, node_sets):
1257
        """Constructor.
1258
        
1259
        Arguments:
1260
        node_sets -- A list of (n,c) pairs (where n is the number of nodes
1261
        in the set and c is a Capacity object; all nodes in the set have
1262
        capacity c).
1263
        """                 
1264
        self.node_sets = node_sets
1265

    
1266
    @classmethod
1267
    def from_xml_element(cls, nodes_element):
1268
        """Constructs a node collection from an ElementTree element.
1269
        
1270
        See the Haizea documentation for details on the
1271
        <nodes> XML format.
1272
        
1273
        Argument:
1274
        element -- Element object containing a "<nodes>" element.
1275
        """           
1276
        nodesets = []
1277
        nodesets_elems = nodes_element.findall("node-set")
1278
        for nodeset_elem in nodesets_elems:
1279
            r = Capacity([])
1280
            resources = nodeset_elem.findall("res")
1281
            for i, res in enumerate(resources):
1282
                res_type = res.get("type")
1283
                if len(res.getchildren()) == 0:
1284
                    amount = int(res.get("amount"))
1285
                    r.set_ninstances(res_type, 1)
1286
                    r.set_quantity(res_type, amount)
1287
                else:
1288
                    instances = res.findall("instance")
1289
                    r.set_ninstances(type, len(instances))
1290
                    for i, instance in enumerate(instances):
1291
                        amount = int(instance.get("amount"))
1292
                        r.set_quantity_instance(type, i+1, amount)
1293
                                     
1294
            numnodes = int(nodeset_elem.get("numnodes"))
1295

    
1296
            nodesets.append((numnodes,r))
1297
            
1298
        return cls(nodesets)
1299
    
1300
    def to_xml(self):
1301
        """Returns an ElementTree XML representation of the nodes
1302
        
1303
        See the Haizea documentation for details on the
1304
        lease XML format.
1305
        
1306
        """   
1307
        nodes = ET.Element("nodes")
1308
        for (numnodes, capacity) in self.node_sets:
1309
            nodeset = ET.SubElement(nodes, "node-set")
1310
            nodeset.set("numnodes", str(numnodes))
1311
            for res_type in capacity.get_resource_types():
1312
                res = ET.SubElement(nodeset, "res")
1313
                res.set("type", res_type)
1314
                ninstances = capacity.get_ninstances(res_type)
1315
                if ninstances == 1:
1316
                    res.set("amount", str(capacity.get_quantity(res_type)))                
1317
            
1318
        return nodes
1319
    
1320
    def get_all_nodes(self):
1321
        """Returns a dictionary mapping individual nodes to capacities
1322
        
1323
        """              
1324
        nodes = {}
1325
        nodenum = 1
1326
        for node_set in self.node_sets:
1327
            numnodes = node_set[0]
1328
            r = node_set[1]
1329
            for i in range(numnodes):
1330
                nodes[nodenum] = r
1331
                nodenum += 1     
1332
        return nodes   
1333
                
1334
    def add_resource(self, name, amounts):
1335
        """Adds a new resource to all the nodes
1336
                
1337
        Argument:
1338
        name -- Name of the resource type
1339
        amounts -- A list with the amounts of the resource to add to each
1340
        node. If the resource is single-instance, then this will just
1341
        be a list with a single element. If multi-instance, each element
1342
        of the list represent the amount of an instance of the resource.
1343
        """              
1344
        for node_set in self.node_sets:
1345
            r = node_set[1]
1346
            r.set_ninstances(name, len(amounts))
1347
            for ninstance, amount in enumerate(amounts):
1348
                r.set_quantity_instance(name, ninstance+1, amount)
1349