Project

General

Profile

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

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, round_datetime_delta, get_lease_id, pretty_nodemap, xmlrpc_marshall_singlevalue
38
from haizea.core.scheduler.slottable import ResourceReservation
39

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

    
42
import logging
43
import xml.etree.ElementTree as ET
44

    
45

    
46

    
47

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

    
155
        # Bookkeeping attributes:
156

    
157
        # Lease state
158
        if state == None:
159
            state = Lease.STATE_NEW
160
        self.state = LeaseStateMachine(initial_state = state)
161

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

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

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

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

    
244
        state = element.get("state")
245
        if state == None:
246
            state = None
247
        else:
248
            state = int(state)
249

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

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

    
294

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

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

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

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

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

    
430
    def get_scheduled_reservations(self):
431
        """Returns all scheduled reservations
432
                
433
        """           
434
        return [r for r in self.preparation_rrs + self.vm_rrs if r.state == ResourceReservation.STATE_SCHEDULED]
435

    
436
    def get_last_vmrr(self):
437
        """Returns the last VM reservation for this lease.
438
                        
439
        """            
440
        return self.vm_rrs[-1]    
441

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

    
492
    def clear_rrs(self):
493
        """Removes all resource reservations for this lease
494
        (both preparation and VM)
495
        
496
        """            
497
        self.preparation_rrs = []
498
        self.vm_rrs = []
499

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

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

    
668

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

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

    
793
    def __ne__(self, other):
794
        """Tests if two capacities are not the same
795
                        
796
        """        
797
        return not self == other
798
            
799
    def __repr__(self):
800
        """Returns a string representation of the Capacity"""
801
        return "  |  ".join("%s: %i" % (type,q[0]) for type, q in self.quantity.items())
802
            
803

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

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

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

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

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

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

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

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

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

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

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

    
1151
        # Validate nodes
1152
        for node_set in nodes.node_sets:
1153
            capacity = node_set[1]
1154
            for resource_type in capacity.get_resource_types():
1155
                if resource_type not in resource_types:
1156
                    # TODO: Raise something more meaningful
1157
                    raise Exception
1158

    
1159
        return cls(nodes, resource_types, attrs)
1160
    
1161
    @classmethod
1162
    def from_resources_string(cls, resource_str):
1163
        """Constructs a site from a "resources string"
1164
        
1165
        A "resources string" is a shorthand way of specifying a site
1166
        with homogeneous resources and no attributes. The format is:
1167
        
1168
        <numnodes> <resource_type>:<resource_quantity>[,<resource_type>:<resource_quantity>]*
1169
        
1170
        For example: 4 CPU:100,Memory:1024
1171
        
1172
        Argument:
1173
        resource_str -- resources string
1174
        """    
1175

    
1176
        resource_str = resource_str.split()
1177
        numnodes = int(resource_str[0])
1178
        resources = resource_str[1:]
1179
        res = {}
1180
        
1181
        for r in resources:
1182
            type, amount = r.split(":")
1183
            res[type] = int(amount)
1184
            
1185
        capacity = Capacity(res.keys())
1186
        for (type,amount) in res.items():
1187
            capacity.set_quantity(type, amount)
1188
        
1189
        nodes = Nodes([(numnodes,capacity)])
1190

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

    
1223
        return max_ninstances
1224
    
1225

    
1226

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

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

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