~ubuntu-branches/ubuntu/utopic/maas/utopic-security

« back to all changes in this revision

Viewing changes to src/maasserver/models/node.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Jeroen Vermeulen, Andres Rodriguez, Jason Hobbs, Raphaël Badin, Louis Bouchard, Gavin Panella
  • Date: 2014-08-21 19:36:30 UTC
  • mfrom: (1.3.1)
  • Revision ID: package-import@ubuntu.com-20140821193630-kertpu5hd8yyss8h
Tags: 1.7.0~beta7+bzr3266-0ubuntu1
* New Upstream Snapshot, Beta 7 bzr3266

[ Jeroen Vermeulen ]
* debian/extras/99-maas-sudoers
  debian/maas-dhcp.postinst
  debian/rules
  - Add second DHCP server instance for IPv6.
* debian/maas-region-controller-min.install
  debian/maas-region-controller-min.lintian-overrides
  - Install deployment user-data: maas_configure_interfaces.py script.
* debian/maas-cluster-controller.links
  debian/maas-cluster-controller.install
  debian/maas-cluster-controller.postinst
  - Reflect Celery removal changes made in trunk r3067.
  - Don't install celeryconfig_cluster.py any longer. 
  - Don't install maas_local_celeryconfig_cluster.py any longer.
  - Don't symlink maas_local_celeryconfig_cluster.py from /etc to /usr.
  - Don't insert UUID into maas_local_celeryconfig_cluster.py.

[ Andres Rodriguez ]
* debian/maas-region-controller-min.postrm: Cleanup lefover files.
* debian/maas-dhcp.postrm: Clean leftover configs.
* Provide new maas-proxy package that replaces the usage of
  squid-deb-proxy:
  - debian/control: New maas-proxy package that replaces the usage
    of squid-deb-proxy; Drop depends on squid-deb-proxy.
  - Add upstrart job.
  - Ensure squid3 is stopped as maas-proxy uses a caching proxy.
* Remove Celery references to cluster controller:
  - Rename upstart job from maas-pserv to maas-cluster; rename
    maas-cluster-celery to maas-cluster-register. Ensure services
    are stopped on upgrade.
  - debian/maintscript: Cleanup config files.
  - Remove all references to the MAAS celery daemon and config
    files as we don't use it like that anymore
* Move some entries in debian/maintscript to
  debian/maas-cluster-controller.maintscript
* Remove usage of txlongpoll and rabbitmq-server. Handle upgrades
  to ensure these are removed correctly.

[ Jason Hobbs ]
* debian/maas-region-controller-min.install: Install
  maas-generate-winrm-cert script.

[ Raphaël Badin ]
* debian/extras/maas-region-admin: Bypass django-admin as it prints
  spurious messages to stdout (LP: #1365130).

[Louis Bouchard]
* debian/maas-cluster-controller.postinst:
  - Exclude /var/log/maas/rsyslog when changing ownership
    (LP: #1346703)

[Gavin Panella]
* debian/maas-cluster-controller.maas-clusterd.upstart:
  - Don't start-up the cluster controller unless a shared-secret has
    been installed.
* debian/maas-cluster-controller.maas-cluster-register.upstart: Drop.

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
 
14
14
__metaclass__ = type
15
15
__all__ = [
16
 
    "NODE_TRANSITIONS",
17
16
    "Node",
18
17
    "fqdn_is_duplicate",
19
18
    "nodegroup_fqdn",
20
19
    ]
21
20
 
22
 
from collections import namedtuple
 
21
 
 
22
from collections import (
 
23
    defaultdict,
 
24
    namedtuple,
 
25
    )
 
26
from datetime import (
 
27
    datetime,
 
28
    timedelta,
 
29
    )
23
30
from itertools import chain
24
31
import re
25
32
from string import whitespace
26
33
from uuid import uuid1
27
34
 
28
 
import celery
 
35
import crochet
29
36
from django.contrib.auth.models import User
30
37
from django.core.exceptions import (
31
38
    PermissionDenied,
32
39
    ValidationError,
33
40
    )
 
41
from django.db import transaction
34
42
from django.db.models import (
35
43
    BooleanField,
36
44
    CharField,
45
53
from django.shortcuts import get_object_or_404
46
54
import djorm_pgarray.fields
47
55
from maasserver import DefaultMeta
 
56
from maasserver.clusterrpc.dhcp import (
 
57
    remove_host_maps,
 
58
    update_host_maps,
 
59
    )
 
60
from maasserver.clusterrpc.power import (
 
61
    power_off_nodes,
 
62
    power_on_nodes,
 
63
    )
48
64
from maasserver.enum import (
49
65
    NODE_BOOT,
50
66
    NODE_BOOT_CHOICES,
55
71
    NODEGROUPINTERFACE_MANAGEMENT,
56
72
    POWER_STATE,
57
73
    POWER_STATE_CHOICES,
 
74
    PRESEED_TYPE,
58
75
    )
59
76
from maasserver.exceptions import (
60
77
    NodeStateViolation,
69
86
from maasserver.models.config import Config
70
87
from maasserver.models.dhcplease import DHCPLease
71
88
from maasserver.models.licensekey import LicenseKey
72
 
from maasserver.models.staticipaddress import (
73
 
    StaticIPAddress,
74
 
    StaticIPAddressExhaustion,
 
89
from maasserver.models.macaddress import (
 
90
    MACAddress,
 
91
    update_mac_cluster_interfaces,
75
92
    )
 
93
from maasserver.models.staticipaddress import StaticIPAddress
76
94
from maasserver.models.tag import Tag
77
95
from maasserver.models.timestampedmodel import TimestampedModel
78
96
from maasserver.models.zone import Zone
 
97
from maasserver.node_status import (
 
98
    COMMISSIONING_LIKE_STATUSES,
 
99
    get_failed_status,
 
100
    is_failed_status,
 
101
    NODE_TRANSITIONS,
 
102
    )
 
103
from maasserver.rpc import getClientFor
79
104
from maasserver.utils import (
80
105
    get_db_state,
81
106
    strip_domain,
82
107
    )
83
108
from netaddr import IPAddress
84
109
from piston.models import Token
85
 
from provisioningserver.drivers.osystem import OperatingSystemRegistry
86
110
from provisioningserver.logger import get_maas_logger
87
 
from provisioningserver.tasks import (
88
 
    add_new_dhcp_host_map,
89
 
    power_off,
90
 
    power_on,
91
 
    remove_dhcp_host_map,
 
111
from provisioningserver.power.poweraction import UnknownPowerType
 
112
from provisioningserver.rpc.cluster import (
 
113
    CancelMonitor,
 
114
    StartMonitors,
92
115
    )
 
116
from provisioningserver.rpc.exceptions import MultipleFailures
 
117
from provisioningserver.rpc.power import QUERY_POWER_TYPES
93
118
from provisioningserver.utils.enum import map_enum_reverse
 
119
from provisioningserver.utils.twisted import asynchronous
 
120
from twisted.internet.defer import DeferredList
 
121
from twisted.protocols import amp
94
122
 
95
123
 
96
124
maaslog = get_maas_logger("node")
97
125
 
98
126
 
 
127
def wait_for_power_commands(deferreds):
 
128
    """Wait for a collection of power command deferreds to return or fail.
 
129
 
 
130
    :param deferreds: A collection of deferreds upon which to wait.
 
131
    :raises: MultipleFailures if any of the deferreds fail.
 
132
    """
 
133
    @asynchronous(timeout=30)
 
134
    def block_until_commands_complete():
 
135
        return DeferredList(deferreds, consumeErrors=True)
 
136
 
 
137
    results = block_until_commands_complete()
 
138
 
 
139
    failures = list(
 
140
        result for success, result in results if not success)
 
141
 
 
142
    if len(failures) != 0:
 
143
        raise MultipleFailures(*failures)
 
144
 
 
145
 
99
146
def generate_node_system_id():
100
147
    return 'node-%s' % uuid1()
101
148
 
102
149
 
103
 
# Information about valid node status transitions.
104
 
# The format is:
105
 
# {
106
 
#  old_status1: [
107
 
#      new_status11,
108
 
#      new_status12,
109
 
#      new_status13,
110
 
#      ],
111
 
# ...
112
 
# }
113
 
#
114
 
NODE_TRANSITIONS = {
115
 
    None: [
116
 
        NODE_STATUS.NEW,
117
 
        NODE_STATUS.MISSING,
118
 
        NODE_STATUS.RETIRED,
119
 
        ],
120
 
    NODE_STATUS.NEW: [
121
 
        NODE_STATUS.COMMISSIONING,
122
 
        NODE_STATUS.MISSING,
123
 
        NODE_STATUS.READY,
124
 
        NODE_STATUS.RETIRED,
125
 
        NODE_STATUS.BROKEN,
126
 
        ],
127
 
    NODE_STATUS.COMMISSIONING: [
128
 
        NODE_STATUS.FAILED_TESTS,
129
 
        NODE_STATUS.READY,
130
 
        NODE_STATUS.RETIRED,
131
 
        NODE_STATUS.MISSING,
132
 
        NODE_STATUS.NEW,
133
 
        NODE_STATUS.BROKEN,
134
 
        ],
135
 
    NODE_STATUS.FAILED_TESTS: [
136
 
        NODE_STATUS.COMMISSIONING,
137
 
        NODE_STATUS.MISSING,
138
 
        NODE_STATUS.RETIRED,
139
 
        NODE_STATUS.BROKEN,
140
 
        ],
141
 
    NODE_STATUS.READY: [
142
 
        NODE_STATUS.COMMISSIONING,
143
 
        NODE_STATUS.ALLOCATED,
144
 
        NODE_STATUS.RESERVED,
145
 
        NODE_STATUS.RETIRED,
146
 
        NODE_STATUS.MISSING,
147
 
        NODE_STATUS.BROKEN,
148
 
        ],
149
 
    NODE_STATUS.RESERVED: [
150
 
        NODE_STATUS.READY,
151
 
        NODE_STATUS.ALLOCATED,
152
 
        NODE_STATUS.RETIRED,
153
 
        NODE_STATUS.MISSING,
154
 
        NODE_STATUS.BROKEN,
155
 
        ],
156
 
    NODE_STATUS.ALLOCATED: [
157
 
        NODE_STATUS.READY,
158
 
        NODE_STATUS.RETIRED,
159
 
        NODE_STATUS.MISSING,
160
 
        NODE_STATUS.BROKEN,
161
 
        ],
162
 
    NODE_STATUS.MISSING: [
163
 
        NODE_STATUS.NEW,
164
 
        NODE_STATUS.READY,
165
 
        NODE_STATUS.ALLOCATED,
166
 
        NODE_STATUS.COMMISSIONING,
167
 
        NODE_STATUS.BROKEN,
168
 
        ],
169
 
    NODE_STATUS.RETIRED: [
170
 
        NODE_STATUS.NEW,
171
 
        NODE_STATUS.READY,
172
 
        NODE_STATUS.MISSING,
173
 
        NODE_STATUS.BROKEN,
174
 
        ],
175
 
    NODE_STATUS.BROKEN: [
176
 
        NODE_STATUS.COMMISSIONING,
177
 
        NODE_STATUS.READY,
178
 
        ],
179
 
    }
180
 
 
181
 
 
182
 
class UnknownPowerType(Exception):
183
 
    """Raised when a node has an unknown power type."""
184
 
 
185
 
 
186
150
def validate_hostname(hostname):
187
151
    """Validator for hostnames.
188
152
 
222
186
 
223
187
# Return type from `get_effective_power_info`.
224
188
PowerInfo = namedtuple("PowerInfo", (
225
 
    "can_be_started", "can_be_stopped", "power_type", "power_parameters"))
 
189
    "can_be_started", "can_be_stopped", "can_be_queried", "power_type",
 
190
    "power_parameters"))
226
191
 
227
192
 
228
193
class NodeManager(Manager):
259
224
        :return: A version of `node` that is filtered to include only those
260
225
            nodes that `user` is allowed to access.
261
226
        """
 
227
        # If the data is corrupt, this can get called with None for
 
228
        # user where a Node should have an owner but doesn't.
 
229
        # Nonetheless, the code should not crash with corrupt data.
 
230
        if user is None:
 
231
            return nodes.none()
262
232
        if user.is_superuser:
263
233
            # Admin is allowed to see all nodes.
264
234
            return nodes
370
340
        :return: Those Nodes for which shutdown was actually requested.
371
341
        :rtype: list
372
342
        """
373
 
        maaslog.debug("Stopping node(s): %s", ids)
 
343
        # Obtain node model objects for each node specified.
374
344
        nodes = self.get_nodes(by_user, NODE_PERMISSION.EDIT, ids=ids)
375
 
        processed_nodes = []
376
 
        for node in nodes:
377
 
            power_params = node.get_effective_power_parameters()
378
 
            try:
379
 
                node_power_type = node.get_effective_power_type()
380
 
            except UnknownPowerType:
381
 
                # Skip the rest of the loop to avoid creating a power
382
 
                # event for a node that we can't power down.
383
 
                maaslog.warning(
384
 
                    "%s: Node has an unknown power type. Not creating "
385
 
                    "power down event.", node.hostname)
386
 
                continue
387
 
            power_params['power_off_mode'] = stop_mode
388
 
            # WAKE_ON_LAN does not support poweroff.
389
 
            if node_power_type != 'ether_wake':
390
 
                maaslog.info(
391
 
                    "%s: Asking cluster to power off node", node.hostname)
392
 
                power_off.apply_async(
393
 
                    queue=node.work_queue, args=[node_power_type],
394
 
                    kwargs=power_params)
395
 
            processed_nodes.append(node)
396
 
        return processed_nodes
 
345
 
 
346
        # Helper function to whittle the list of nodes down to those that we
 
347
        # can actually stop, and keep hold of their power control info.
 
348
        def gen_power_info(nodes):
 
349
            for node in nodes:
 
350
                power_info = node.get_effective_power_info()
 
351
                if power_info.can_be_stopped:
 
352
                    # Smuggle in a hint about how to power-off the node.
 
353
                    power_info.power_parameters['power_off_mode'] = stop_mode
 
354
                    yield node, power_info
 
355
 
 
356
        # Create info that we can pass into the reactor (no model objects).
 
357
        nodes_stop_info = list(
 
358
            (node.system_id, node.hostname, node.nodegroup.uuid, power_info)
 
359
            for node, power_info in gen_power_info(nodes))
 
360
        powered_systems = [
 
361
            system_id for system_id, _, _, _ in nodes_stop_info]
 
362
 
 
363
        # Request that these nodes be powered off and wait for the
 
364
        # commands to return or fail.
 
365
        deferreds = power_off_nodes(nodes_stop_info).viewvalues()
 
366
        wait_for_power_commands(deferreds)
 
367
 
 
368
        # Return a list of those nodes that we've sent power commands for.
 
369
        return list(
 
370
            node for node in nodes if node.system_id in powered_systems)
397
371
 
398
372
    def start_nodes(self, ids, by_user, user_data=None):
399
373
        """Request on given user's behalf that the given nodes be started up.
414
388
        :type user_data: unicode
415
389
        :return: Those Nodes for which power-on was actually requested.
416
390
        :rtype: list
 
391
 
 
392
        :raises MultipleFailures: When there are failures originating from a
 
393
            remote process. There could be one or more failures -- it's not
 
394
            strictly *multiple* -- but they do all originate from comms with
 
395
            remote processes.
 
396
        :raises: `StaticIPAddressExhaustion` if there are not enough IP
 
397
            addresses left in the static range..
417
398
        """
418
 
        maaslog.debug("Starting nodes: %s", ids)
419
399
        # Avoid circular imports.
420
400
        from metadataserver.models import NodeUserData
421
401
 
 
402
        # Obtain node model objects for each node specified.
422
403
        nodes = self.get_nodes(by_user, NODE_PERMISSION.EDIT, ids=ids)
423
 
        for node in nodes:
424
 
            NodeUserData.objects.set_user_data(node, user_data)
425
 
        processed_nodes = []
426
 
        for node in nodes:
427
 
            maaslog.info("%s: Attempting start up", node.hostname)
428
 
            power_params = node.get_effective_power_parameters()
429
 
            try:
430
 
                node_power_type = node.get_effective_power_type()
431
 
            except UnknownPowerType:
432
 
                # Skip the rest of the loop to avoid creating a power
433
 
                # event for a node that we can't power up.
434
 
                maaslog.warning(
435
 
                    "%s: Node has an unknown power type. Not creating "
436
 
                    "power up event.", node.hostname)
437
 
                continue
438
 
            if node_power_type == 'ether_wake':
439
 
                mac = power_params.get('mac_address')
440
 
                do_start = (mac != '' and mac is not None)
441
 
            else:
442
 
                do_start = True
443
 
            if do_start:
444
 
                tasks = []
445
 
                try:
446
 
                    if node.status == NODE_STATUS.ALLOCATED:
447
 
                        tasks.extend(node.claim_static_ips())
448
 
                except StaticIPAddressExhaustion:
449
 
                    maaslog.error(
450
 
                        "%s: Unable to allocate static IP due to "
451
 
                        "address exhaustion.", node.hostname)
452
 
                    # XXX 2014-06-17 bigjools bug=1330762
453
 
                    # This function is supposed to start all the nodes
454
 
                    # it can, but gives no way to return errors about
455
 
                    # the ones it can't.  So just fail the lot for now,
456
 
                    # pending a redesign of the API.
457
 
                    #
458
 
                    # XXX 2014-06-17 bigjools bug=1330765
459
 
                    # If any of this fails it needs to release the
460
 
                    # static IPs back to the pool.  As part of the robustness
461
 
                    # work coming up, it also needs to inform the user.
462
 
                    raise
463
 
 
464
 
                task = power_on.si(node_power_type, **power_params)
465
 
                task.set(queue=node.work_queue)
466
 
                tasks.append(task)
467
 
                chained_tasks = celery.chain(tasks)
468
 
                maaslog.debug("%s: Asking cluster to power on", node.hostname)
469
 
                chained_tasks.apply_async()
470
 
                processed_nodes.append(node)
471
 
        return processed_nodes
 
404
 
 
405
        # Record the same user data for all nodes we've been *requested* to
 
406
        # start, regardless of whether or not we actually can; the user may
 
407
        # choose to manually start them.
 
408
        NodeUserData.objects.bulk_set_user_data(nodes, user_data)
 
409
 
 
410
        # Claim static IP addresses for all nodes we've been *requested* to
 
411
        # start, such that they're recorded in the database. This results in a
 
412
        # mapping of nodegroups to (ips, macs).
 
413
        static_mappings = defaultdict(dict)
 
414
        for node in nodes:
 
415
            if node.status == NODE_STATUS.ALLOCATED:
 
416
                claims = node.claim_static_ip_addresses()
 
417
                static_mappings[node.nodegroup].update(claims)
 
418
                node.start_deployment()
 
419
 
 
420
        # XXX 2014-06-17 bigjools bug=1330765
 
421
        # If the above fails it needs to release the static IPs back to the
 
422
        # pool. An enclosing transaction or savepoint from the caller may take
 
423
        # care of this, given that a serious problem above will result in an
 
424
        # exception. If we're being belt-n-braces though it ought to clear up
 
425
        # before returning too. As part of the robustness work coming up, it
 
426
        # also needs to inform the user.
 
427
 
 
428
        # Update host maps and wait for them so that we can report failures
 
429
        # directly to the caller.
 
430
        update_host_maps_failures = list(update_host_maps(static_mappings))
 
431
        if len(update_host_maps_failures) != 0:
 
432
            raise MultipleFailures(*update_host_maps_failures)
 
433
 
 
434
        # Update the DNS zone with the new static IP info as necessary.
 
435
        from maasserver.dns.config import change_dns_zones
 
436
        change_dns_zones({node.nodegroup for node in nodes})
 
437
 
 
438
        # Helper function to whittle the list of nodes down to those that we
 
439
        # can actually start, and keep hold of their power control info.
 
440
        def gen_power_info(nodes):
 
441
            for node in nodes:
 
442
                power_info = node.get_effective_power_info()
 
443
                if power_info.can_be_started:
 
444
                    yield node, power_info
 
445
 
 
446
        # Create info that we can pass into the reactor (no model objects).
 
447
        nodes_start_info = list(
 
448
            (node.system_id, node.hostname, node.nodegroup.uuid, power_info)
 
449
            for node, power_info in gen_power_info(nodes))
 
450
        powered_systems = [
 
451
            system_id for system_id, _, _, _ in nodes_start_info]
 
452
 
 
453
        # Request that these nodes be powered off and wait for the
 
454
        # commands to return or fail.
 
455
        deferreds = power_on_nodes(nodes_start_info).viewvalues()
 
456
        wait_for_power_commands(deferreds)
 
457
 
 
458
        # Return a list of those nodes that we've sent power commands for.
 
459
        return list(
 
460
            node for node in nodes if node.system_id in powered_systems)
472
461
 
473
462
 
474
463
def patch_pgarray_types():
520
509
    return False
521
510
 
522
511
 
 
512
# List of statuses for which it makes sense to release a node.
 
513
RELEASABLE_STATUSES = [
 
514
    NODE_STATUS.ALLOCATED,
 
515
    NODE_STATUS.RESERVED,
 
516
    NODE_STATUS.BROKEN,
 
517
    NODE_STATUS.DEPLOYING,
 
518
    NODE_STATUS.DEPLOYED,
 
519
    NODE_STATUS.FAILED_DEPLOYMENT,
 
520
    ]
 
521
 
 
522
 
523
523
class Node(CleanSave, TimestampedModel):
524
524
    """A `Node` represents a physical machine used by the MAAS Server.
525
525
 
636
636
    disable_ipv4 = BooleanField(
637
637
        default=False, verbose_name="Disable IPv4 when deployed",
638
638
        help_text=(
639
 
            "On operating systems where this choice is supported, "
640
 
            "disable IPv4 networking on this node when it is deployed.  "
641
 
            "IPv4 may still be used for booting and installing the node."))
 
639
            "On operating systems where this choice is supported, this option "
 
640
            "disables IPv4 networking on this node when it is deployed.  "
 
641
            "IPv4 may still be used for booting and installing the node.  "
 
642
            "THIS MAY STOP YOUR NODE FROM WORKING.  Do not disable IPv4 "
 
643
            "unless you know what you're doing: clusters must be configured "
 
644
            "to use a MAAS URL with a hostname that resolves on both IPv4 and "
 
645
            "IPv6."))
 
646
 
 
647
    # Record the MAC address for the interface the node last PXE booted from.
 
648
    # This will be used for determining which MAC address to create a static
 
649
    # IP reservation for when starting a node.
 
650
    pxe_mac = ForeignKey(
 
651
        MACAddress, default=None, blank=True, null=True, editable=False,
 
652
        related_name='+')
642
653
 
643
654
    objects = NodeManager()
644
655
 
661
672
            return nodegroup_fqdn(self.hostname, self.nodegroup.name)
662
673
        return self.hostname
663
674
 
664
 
    def claim_static_ips(self):
665
 
        """Assign AUTO static IPs for our MACs and return a list of
666
 
        Celery tasks that need executing.  If nothing needs executing,
667
 
        the empty list is returned.
668
 
 
669
 
        Each MAC on the node that is connected to a managed cluster
670
 
        interface will get an IP.
671
 
 
672
 
        This operation is atomic; if claiming an IP on a particular MAC fails
673
 
        then none of the MACs will get an IP and StaticIPAddressExhaustion
674
 
        is raised.
675
 
        """
676
 
        try:
677
 
            tasks = self._create_tasks_for_static_ips()
678
 
        except StaticIPAddressExhaustion:
679
 
            StaticIPAddress.objects.deallocate_by_node(self)
680
 
            raise
681
 
 
682
 
        # Update the DNS zone with the new static IP info as necessary.
683
 
        from maasserver.dns.config import change_dns_zones
684
 
        change_dns_zones([self.nodegroup])
685
 
        return tasks
686
 
 
687
 
    def _create_hostmap_task(self, mac, sip):
688
 
        # This is creating a list of celery 'Signatures' which will be
689
 
        # chained together later.  Normally the result of each
690
 
        # chained task is passed to the next, but we don't want that
691
 
        # here.  We can avoid it by making the Signatures
692
 
        # "immutable", and this is done with the "si()" call on the
693
 
        # task, which produces an immutable Signature.
694
 
        # See docs.celeryproject.org/en/latest/userguide/canvas.html
695
 
        dhcp_key = self.nodegroup.dhcp_key
696
 
        mapping = {sip.ip: mac.mac_address.get_raw()}
697
 
        # XXX See bug 1039362 regarding server_address.
698
 
        dhcp_task = add_new_dhcp_host_map.si(
699
 
            mappings=mapping, server_address='127.0.0.1',
700
 
            shared_key=dhcp_key)
701
 
        dhcp_task.set(queue=self.work_queue)
702
 
        return dhcp_task
703
 
 
704
 
    def _create_tasks_for_static_ips(self):
705
 
        tasks = []
706
 
        # Get a new AUTO static IP for each MAC on a managed interface.
707
 
        macs = self.mac_addresses_on_managed_interfaces()
708
 
        for mac in macs:
709
 
            try:
710
 
                sips = mac.claim_static_ips()
711
 
            except StaticIPAddressTypeClash:
712
 
                # There's already a non-AUTO IP, so nothing to do.
713
 
                continue
714
 
            # "sip" may be None if the static range is not yet
715
 
            # defined, which will be the case when migrating from older
716
 
            # versions of the code.  If it is None we just ignore this
717
 
            # MAC.
718
 
            for sip in sips:
719
 
                tasks.append(self._create_hostmap_task(mac, sip))
720
 
                maaslog.info(
721
 
                    "%s: Claimed static IP %s on %s", self.hostname,
722
 
                    sip.ip, mac.mac_address.get_raw())
723
 
        if len(tasks) > 0:
724
 
            # Delete any existing dynamic maps as the first task.  This
725
 
            # is a belt and braces approach to deal with legacy code
726
 
            # that previously used dynamic IPs for hosts.
727
 
            del_existing = self._build_dynamic_host_map_deletion_task()
728
 
            if del_existing is not None:
729
 
                # del_existing is a chain so does not need an explicit
730
 
                # queue to be set as each subtask will have one.
731
 
                tasks.insert(0, del_existing)
732
 
        return tasks
 
675
    def get_deployment_time(self):
 
676
        """Return the deployment time of this node (in seconds)."""
 
677
        # Return a *very* conservative estimate for now.
 
678
        # Something that shouldn't conflict with any deployment.
 
679
        return timedelta(minutes=40).total_seconds()
 
680
 
 
681
    def start_deployment(self):
 
682
        """Mark a node as being deployed."""
 
683
        self.status = NODE_STATUS.DEPLOYING
 
684
        self.save()
 
685
        # We explicitly commit here because during bulk node actions we
 
686
        # want to make sure that each successful state transition is
 
687
        # recorded in the DB.
 
688
        transaction.commit()
 
689
        deployment_time = self.get_deployment_time()
 
690
        self.start_transition_monitor(deployment_time)
 
691
 
 
692
    def end_deployment(self):
 
693
        """Mark a node as successfully deployed."""
 
694
        self.status = NODE_STATUS.DEPLOYED
 
695
        self.save()
 
696
        # We explicitly commit here because during bulk node actions we
 
697
        # want to make sure that each successful state transition is
 
698
        # recorded in the DB.
 
699
        transaction.commit()
 
700
 
 
701
    def start_transition_monitor(self, timeout):
 
702
        """Start cluster-side transition monitor."""
 
703
        context = {
 
704
            'node_status': self.status,
 
705
            'timeout': timeout,
 
706
            }
 
707
        deadline = datetime.now(tz=amp.utc) + timedelta(seconds=timeout)
 
708
        monitors = [{
 
709
            'deadline': deadline,
 
710
            'id': self.system_id,
 
711
            'context': context,
 
712
        }]
 
713
        client = getClientFor(self.nodegroup.uuid)
 
714
        call = client(StartMonitors, monitors=monitors)
 
715
        try:
 
716
            call.wait(5)
 
717
        except crochet.TimeoutError as error:
 
718
            maaslog.error(
 
719
                "%s: Unable to start transition monitor: %s",
 
720
                self.hostname, error)
 
721
        maaslog.info("%s: Starting monitor: %s", self.hostname, monitors[0])
 
722
 
 
723
    def stop_transition_monitor(self):
 
724
        """Stop cluster-side transition monitor."""
 
725
        client = getClientFor(self.nodegroup.uuid)
 
726
        call = client(CancelMonitor, id=self.system_id)
 
727
        try:
 
728
            call.wait(5)
 
729
        except crochet.TimeoutError as error:
 
730
            maaslog.error(
 
731
                "%s: Unable to stop transition monitor: %s",
 
732
                self.hostname, error)
 
733
        maaslog.info("%s: Stopping monitor: %s", self.hostname, self.system_id)
 
734
 
 
735
    def handle_monitor_expired(self, context):
 
736
        """Handle a monitor expired event."""
 
737
        failed_status = get_failed_status(self.status)
 
738
        if failed_status is not None:
 
739
            timeout_timedelta = timedelta(seconds=context['timeout'])
 
740
            self.mark_failed(
 
741
                "Node operation '%s' timed out after %s." % (
 
742
                    (
 
743
                        NODE_STATUS_CHOICES_DICT[self.status],
 
744
                        timeout_timedelta
 
745
                    )))
733
746
 
734
747
    def ip_addresses(self):
735
748
        """IP addresses allocated to this node.
805
818
            query = dhcpleases_qs.filter(mac__in=macs)
806
819
            return query.values_list('ip', flat=True)
807
820
 
 
821
    def get_static_ip_mappings(self):
 
822
        """Return node's static addresses, and their MAC addresses.
 
823
 
 
824
        :return: A list of (IP, MAC) tuples, both in string form.
 
825
        """
 
826
        macs = self.macaddress_set.all().prefetch_related('ip_addresses')
 
827
        return [
 
828
            (sip.ip, mac.mac_address)
 
829
            for mac in macs
 
830
            for sip in mac.ip_addresses.all()
 
831
            ]
 
832
 
808
833
    def mac_addresses_on_managed_interfaces(self):
809
834
        """Return MACAddresses for this node that have a managed cluster
810
835
        interface."""
878
903
 
879
904
        mac = MACAddress(mac_address=mac_address, node=self)
880
905
        mac.save()
 
906
 
 
907
        # See if there's a lease for this MAC and set its
 
908
        # cluster_interface if so.
 
909
        nodegroup_leases = {
 
910
            lease.ip: lease.mac
 
911
            for lease in DHCPLease.objects.filter(nodegroup=self.nodegroup)}
 
912
        update_mac_cluster_interfaces(nodegroup_leases, self.nodegroup)
 
913
 
881
914
        return mac
882
915
 
883
916
    def remove_mac_address(self, mac_address):
920
953
    def start_commissioning(self, user):
921
954
        """Install OS and self-test a new node."""
922
955
        # Avoid circular imports.
923
 
        from metadataserver.commissioning.user_data import generate_user_data
 
956
        from metadataserver.user_data.commissioning import generate_user_data
924
957
        from metadataserver.models import NodeResult
925
958
 
926
 
        commissioning_user_data = generate_user_data(nodegroup=self.nodegroup)
 
959
        commissioning_user_data = generate_user_data(node=self)
927
960
        NodeResult.objects.clear_results(self)
 
961
        # The commissioning profile is handled in start_nodes.
 
962
        maaslog.info(
 
963
            "%s: Starting commissioning", self.hostname)
 
964
        # We need to mark the node as COMMISSIONING now to avoid a race
 
965
        # when starting multiple nodes. We hang on to old_status just in
 
966
        # case the power action fails.
 
967
        old_status = self.status
928
968
        self.status = NODE_STATUS.COMMISSIONING
929
969
        self.save()
930
 
        # The commissioning profile is handled in start_nodes.
931
 
        maaslog.info(
932
 
            "%s: Starting commissioning for", self.hostname)
933
 
        Node.objects.start_nodes(
934
 
            [self.system_id], user, user_data=commissioning_user_data)
 
970
        transaction.commit()
 
971
        try:
 
972
            # We don't check for which nodes we've started here, because
 
973
            # it's possible we can't start the node - its power type may not
 
974
            # allow us to do that.
 
975
            Node.objects.start_nodes(
 
976
                [self.system_id], user, user_data=commissioning_user_data)
 
977
        except Exception as ex:
 
978
            maaslog.error(
 
979
                "%s: Unable to start node: %s",
 
980
                self.hostname, unicode(ex))
 
981
            self.status = old_status
 
982
            self.save()
 
983
            transaction.commit()
 
984
            # Let the exception bubble up, since the UI or API will have to
 
985
            # deal with it.
 
986
            raise
 
987
        else:
 
988
            maaslog.info("%s: Commissioning started", self.hostname)
935
989
 
936
990
    def abort_commissioning(self, user):
937
991
        """Power off a commissioning node and set its status to 'declared'."""
942
996
                % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
943
997
        maaslog.info(
944
998
            "%s: Aborting commissioning", self.hostname)
945
 
        stopped_node = Node.objects.stop_nodes([self.system_id], user)
946
 
        if len(stopped_node) == 1:
 
999
        try:
 
1000
            # We don't check for which nodes we've stopped here, because
 
1001
            # it's possible we can't stop the node - its power type may
 
1002
            # not allow us to do that.
 
1003
            Node.objects.stop_nodes([self.system_id], user)
 
1004
        except Exception as ex:
 
1005
            maaslog.error(
 
1006
                "%s: Unable to shut node down: %s",
 
1007
                self.hostname, unicode(ex))
 
1008
            raise
 
1009
        else:
947
1010
            self.status = NODE_STATUS.NEW
948
1011
            self.save()
 
1012
            maaslog.info("%s: Commissioning aborted", self.hostname)
949
1013
 
950
1014
    def delete(self):
 
1015
        """Delete this node.
 
1016
 
 
1017
        :raises MultipleFailures: If host maps cannot be deleted.
 
1018
        """
951
1019
        # Allocated nodes can't be deleted.
952
1020
        if self.status == NODE_STATUS.ALLOCATED:
953
1021
            raise NodeStateViolation(
954
1022
                "Cannot delete node %s: node is in state %s."
955
1023
                % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
 
1024
 
956
1025
        maaslog.info("%s: Deleting node", self.hostname)
957
 
        # Delete any dynamic host maps in the DHCP server.  This is only
958
 
        # here to cope with legacy code that used to create these, the
959
 
        # current code does not.
960
 
        self._delete_dynamic_host_maps()
961
1026
 
962
 
        # Delete all remaining static IPs.
 
1027
        # Ensure that all static IPs are deleted, and keep track of the IP
 
1028
        # addresses so we can delete the associated host maps.
963
1029
        static_ips = StaticIPAddress.objects.delete_by_node(self)
964
 
        self.delete_static_host_maps(static_ips)
965
 
 
966
 
        # Delete the related mac addresses.
967
 
        # The DHCPLease objects corresponding to these MACs will be deleted
968
 
        # as well. See maasserver/models/dhcplease:delete_lease().
 
1030
        # Collect other IP addresses (likely in the dynamic range) that we
 
1031
        # should delete host maps for. We need to do this because MAAS used to
 
1032
        # declare host maps in the dynamic range. At some point we can stop
 
1033
        # removing host maps from the dynamic range, once we decide that
 
1034
        # enough time has passed.
 
1035
        macs = self.mac_addresses_on_managed_interfaces().values_list(
 
1036
            'mac_address', flat=True)
 
1037
        leases = DHCPLease.objects.filter(
 
1038
            nodegroup=self.nodegroup, mac__in=macs)
 
1039
        leased_ips = leases.values_list("ip", flat=True)
 
1040
        # Delete host maps for all addresses linked to this node.
 
1041
        self.delete_host_maps(set().union(static_ips, leased_ips))
 
1042
        # Delete the related mac addresses. The DHCPLease objects
 
1043
        # corresponding to these MACs will be deleted as well. See
 
1044
        # maasserver/models/dhcplease:delete_lease().
969
1045
        self.macaddress_set.all().delete()
970
1046
 
971
1047
        super(Node, self).delete()
972
1048
 
973
 
    def delete_static_host_maps(self, for_ips):
974
 
        """Delete any host maps for static IPs allocated to this node.
975
 
 
976
 
        :param for_ips: Delete the maps for these IP addresses only.
977
 
        """
978
 
        tasks = []
979
 
        for ip in for_ips:
980
 
            task = remove_dhcp_host_map.si(
981
 
                ip_address=ip, server_address="127.0.0.1",
982
 
                omapi_key=self.nodegroup.dhcp_key)
983
 
            task.set(queue=self.work_queue)
984
 
            tasks.append(task)
985
 
        if len(tasks) > 0:
986
 
            maaslog.info(
987
 
                "%s: Asking cluster to delete static host maps", self.hostname)
988
 
            chain = celery.chain(tasks)
989
 
            chain.apply_async()
990
 
 
991
 
    def _build_dynamic_host_map_deletion_task(self):
992
 
        """Create a chained celery task that will delete this node's
993
 
        dynamic dhcp host maps.
994
 
 
995
 
        Host maps in the DHCP server that are as a result of StaticIPAddresses
996
 
        are not deleted here as these get deleted when nodes are released
997
 
        (for AUTO types) or from a separate user-driven action.
998
 
 
999
 
        Return None if there is nothing to delete.
1000
 
        """
1001
 
        nodegroup = self.nodegroup
1002
 
        if len(nodegroup.get_managed_interfaces()) == 0:
1003
 
            return None
1004
 
 
1005
 
        macs = self.macaddress_set.values_list('mac_address', flat=True)
1006
 
        static_ips = StaticIPAddress.objects.filter(
1007
 
            macaddress__mac_address__in=macs).values_list("ip", flat=True)
1008
 
        # See [1] below for a comment about this use of list():
1009
 
        leases = DHCPLease.objects.filter(
1010
 
            mac__in=macs, nodegroup=nodegroup).exclude(
1011
 
            ip__in=list(static_ips))
1012
 
        tasks = []
1013
 
        for lease in leases:
1014
 
            # XXX See bug 1039362 regarding server_address
1015
 
            task_kwargs = dict(
1016
 
                ip_address=lease.ip,
1017
 
                server_address="127.0.0.1",
1018
 
                omapi_key=nodegroup.dhcp_key)
1019
 
            task = remove_dhcp_host_map.si(**task_kwargs)
1020
 
            task.set(queue=self.work_queue)
1021
 
            tasks.append(task)
1022
 
        if len(tasks) > 0:
1023
 
            return celery.chain(tasks)
1024
 
        return None
1025
 
 
1026
 
        # [1]
1027
 
        # Django has a bug (I know, you're shocked, right?) where it
1028
 
        # casts the outer part of the IN query to a string (from inet
1029
 
        # type) but fails to cast the result of the subselect arising
1030
 
        # from the ValuesQuerySet that values_list() produces. The
1031
 
        # result of that is that you get a Postgres ProgrammingError
1032
 
        # because of the type mismatch.  This bug is avoided by
1033
 
        # listifying the static_ips which vastly simplifies the generated
1034
 
        # SQL as it avoids the subselect.
1035
 
 
1036
 
    def _delete_dynamic_host_maps(self):
1037
 
        """If any DHCPLeases exist for this node, remove any associated
1038
 
        host maps."""
1039
 
        chain = self._build_dynamic_host_map_deletion_task()
1040
 
        if chain is not None:
1041
 
            chain.apply_async()
 
1049
    def delete_host_maps(self, for_ips):
 
1050
        """Delete any host maps for IPs allocated to this node.
 
1051
 
 
1052
        This should probably live on `NodeGroup`.
 
1053
 
 
1054
        :param for_ips: The set of IP addresses to remove host maps for.
 
1055
        :type for_ips: `set`
 
1056
 
 
1057
        :raises MultipleFailures: When there are failures originating from a
 
1058
            remote process. There could be one or more failures -- it's not
 
1059
            strictly *multiple* -- but they do all originate from comms with
 
1060
            remote processes.
 
1061
        """
 
1062
        assert isinstance(for_ips, set), "%r is not a set" % (for_ips,)
 
1063
        if len(for_ips) > 0:
 
1064
            maaslog.info("%s: Deleting DHCP host maps", self.hostname)
 
1065
            removal_mapping = {self.nodegroup: for_ips}
 
1066
            remove_host_maps_failures = list(
 
1067
                remove_host_maps(removal_mapping))
 
1068
            if len(remove_host_maps_failures) != 0:
 
1069
                raise MultipleFailures(*remove_host_maps_failures)
1042
1070
 
1043
1071
    def set_random_hostname(self):
1044
1072
        """Set `hostname` from a shuffled list of candidate names.
1122
1150
            self.distro_series == '')
1123
1151
        if use_default_osystem and use_default_distro_series:
1124
1152
            return Config.objects.get_config('default_distro_series')
1125
 
        elif use_default_distro_series:
1126
 
            osystem = OperatingSystemRegistry[self.osystem]
1127
 
            return osystem.get_default_release()
1128
1153
        else:
1129
1154
            return self.distro_series
1130
1155
 
1191
1216
            if primary_mac is not None:
1192
1217
                mac = primary_mac.mac_address.get_raw()
1193
1218
                power_params['mac_address'] = mac
 
1219
 
 
1220
        # boot_mode is something that tells the template whether this is
 
1221
        # a PXE boot or a local HD boot.
 
1222
        if self.status == NODE_STATUS.DEPLOYED:
 
1223
            power_params['boot_mode'] = 'local'
 
1224
        else:
 
1225
            power_params['boot_mode'] = 'pxe'
 
1226
 
1194
1227
        return power_params
1195
1228
 
1196
1229
    def get_effective_power_info(self):
1214
1247
            power_type = self.get_effective_power_type()
1215
1248
        except UnknownPowerType:
1216
1249
            maaslog.warning("%s: Unrecognised power type.", self.hostname)
1217
 
            return PowerInfo(False, False, None, None)
 
1250
            return PowerInfo(False, False, False, None, None)
1218
1251
        else:
1219
1252
            if power_type == 'ether_wake':
1220
1253
                mac = power_params.get('mac_address')
1223
1256
            else:
1224
1257
                can_be_started = True
1225
1258
                can_be_stopped = True
 
1259
            can_be_queried = power_type in QUERY_POWER_TYPES
1226
1260
            return PowerInfo(
1227
 
                can_be_started, can_be_stopped,
 
1261
                can_be_started, can_be_stopped, can_be_queried,
1228
1262
                power_type, power_params,
1229
1263
            )
1230
1264
 
1239
1273
        self.save()
1240
1274
        maaslog.info("%s allocated to user %s", self.hostname, user.username)
1241
1275
 
 
1276
    def start_disk_erasing(self, user):
 
1277
        """Erase the disks on a node."""
 
1278
        # Avoid circular imports.
 
1279
        from metadataserver.user_data.disk_erasing import generate_user_data
 
1280
 
 
1281
        disk_erase_user_data = generate_user_data(node=self)
 
1282
        maaslog.info(
 
1283
            "%s: Starting disk erasure", self.hostname)
 
1284
        # Change the status of the node now to avoid races when starting
 
1285
        # nodes in bulk.
 
1286
        self.status = NODE_STATUS.DISK_ERASING
 
1287
        self.save()
 
1288
        transaction.commit()
 
1289
        try:
 
1290
            Node.objects.start_nodes(
 
1291
                [self.system_id], user, user_data=disk_erase_user_data)
 
1292
        except Exception as ex:
 
1293
            maaslog.error(
 
1294
                "%s: Unable to start node: %s",
 
1295
                self.hostname, unicode(ex))
 
1296
            # We always mark the node as failed here, although we could
 
1297
            # potentially move it back to the state it was in
 
1298
            # previously. For now, though, this is safer, since it marks
 
1299
            # the node as needing attention.
 
1300
            self.status = NODE_STATUS.FAILED_DISK_ERASING
 
1301
            self.save()
 
1302
            transaction.commit()
 
1303
            raise
 
1304
        else:
 
1305
            maaslog.info(
 
1306
                "%s: Disk erasure started.", self.hostname)
 
1307
 
 
1308
    def abort_disk_erasing(self, user):
 
1309
        """
 
1310
        Power off disk erasing node and set its status to 'failed disk
 
1311
        erasing'.
 
1312
        """
 
1313
        if self.status != NODE_STATUS.DISK_ERASING:
 
1314
            raise NodeStateViolation(
 
1315
                "Cannot abort disk erasing of a non disk erasing node: "
 
1316
                "node %s is in state %s."
 
1317
                % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
 
1318
        maaslog.info(
 
1319
            "%s: Aborting disk erasing", self.hostname)
 
1320
        try:
 
1321
            Node.objects.stop_nodes([self.system_id], user)
 
1322
        except Exception as ex:
 
1323
            maaslog.error(
 
1324
                "%s: Unable to shut node down: %s",
 
1325
                self.hostname, unicode(ex))
 
1326
            raise
 
1327
        else:
 
1328
            self.status = NODE_STATUS.FAILED_DISK_ERASING
 
1329
            self.save()
 
1330
 
 
1331
    def abort_operation(self, user):
 
1332
        """Abort the current operation.
 
1333
        This currently only supports aborting Disk Erasing.
 
1334
        """
 
1335
        if self.status == NODE_STATUS.DISK_ERASING:
 
1336
            self.abort_disk_erasing(user)
 
1337
            return
 
1338
 
 
1339
        raise NodeStateViolation(
 
1340
            "Cannot abort in current state: "
 
1341
            "node %s is in state %s."
 
1342
            % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
 
1343
 
1242
1344
    def release(self):
1243
 
        """Mark allocated or reserved node as available again and power off."""
 
1345
        """Mark allocated or reserved node as available again and power off.
 
1346
 
 
1347
        :raises MultipleFailures: If host maps cannot be deleted.
 
1348
        """
1244
1349
        maaslog.info("%s: Releasing node", self.hostname)
1245
 
        Node.objects.stop_nodes([self.system_id], self.owner)
 
1350
        try:
 
1351
            Node.objects.stop_nodes([self.system_id], self.owner)
 
1352
        except Exception as ex:
 
1353
            maaslog.error(
 
1354
                "%s: Unable to shut node down: %s", self.hostname,
 
1355
                unicode(ex))
 
1356
            raise
 
1357
 
1246
1358
        deallocated_ips = StaticIPAddress.objects.deallocate_by_node(self)
1247
 
        self.delete_static_host_maps(deallocated_ips)
 
1359
        self.delete_host_maps(deallocated_ips)
1248
1360
        from maasserver.dns.config import change_dns_zones
1249
1361
        change_dns_zones([self.nodegroup])
1250
 
        # Belt-and-braces: we should never reach a point where the node
1251
 
        # is BROKEN and still allocated, since mark_broken() releases
1252
 
        # the node anyway, but bug 1351451 seemed to suggest that it was
1253
 
        # possible. This can be removed if that proves not to be the
1254
 
        # case.
1255
 
        if self.status != NODE_STATUS.BROKEN:
1256
 
            self.status = NODE_STATUS.READY
1257
 
        self.owner = None
 
1362
        if self.power_state == POWER_STATE.OFF:
 
1363
            # Node is already off.
 
1364
            self.status = NODE_STATUS.READY
 
1365
            self.owner = None
 
1366
        elif self.get_effective_power_info().can_be_queried:
 
1367
            # Controlled power type (one for which we can query the power
 
1368
            # state): update_power_state() will take care of making the node
 
1369
            # READY and unowned when the power is finally off.
 
1370
            self.status = NODE_STATUS.RELEASING
 
1371
        else:
 
1372
            # Uncontrolled power type (one for which we can't query the power
 
1373
            # state): mark the node ready.
 
1374
            self.status = NODE_STATUS.READY
 
1375
            self.owner = None
1258
1376
        self.token = None
1259
1377
        self.agent_name = ''
1260
1378
        self.set_netboot()
1262
1380
        self.distro_series = ''
1263
1381
        self.license_key = ''
1264
1382
        self.save()
 
1383
        # We explicitly commit here because during bulk node actions we
 
1384
        # want to make sure that each successful state transition is
 
1385
        # recorded in the DB.
 
1386
        transaction.commit()
 
1387
 
 
1388
    def release_or_erase(self):
 
1389
        """Either release the node or erase the node then release it, depending
 
1390
        on settings."""
 
1391
        erase_on_release = Config.objects.get_config(
 
1392
            'enable_disk_erasing_on_release')
 
1393
        if erase_on_release:
 
1394
            self.start_disk_erasing(self.owner)
 
1395
            return
 
1396
 
 
1397
        self.release()
1265
1398
 
1266
1399
    def set_netboot(self, on=True):
1267
1400
        """Set netboot on or off."""
1271
1404
 
1272
1405
    def get_deployment_status(self):
1273
1406
        """Return a string repr of the deployment status of this node."""
1274
 
        if self.status == NODE_STATUS.BROKEN:
1275
 
            return "Broken"
1276
 
        if self.owner is None:
1277
 
            return "Unused"
1278
 
        if self.netboot:
1279
 
            return "Deploying"
1280
 
        return "Deployed"
 
1407
        mapping = {
 
1408
            NODE_STATUS.DEPLOYED: "Deployed",
 
1409
            NODE_STATUS.DEPLOYING: "Deploying",
 
1410
            NODE_STATUS.FAILED_DEPLOYMENT: "Failed deployment",
 
1411
        }
 
1412
        return mapping.get(self.status, "Not in deployment")
1281
1413
 
1282
1414
    def split_arch(self):
1283
1415
        """Return architecture and subarchitecture, as a tuple."""
1284
1416
        arch, subarch = self.architecture.split('/')
1285
1417
        return (arch, subarch)
1286
1418
 
 
1419
    def mark_failed(self, error_description):
 
1420
        """Mark this node as failed.
 
1421
 
 
1422
        The actual 'failed' state depends on the current status of the
 
1423
        node.
 
1424
        """
 
1425
        new_status = get_failed_status(self.status)
 
1426
        if new_status is not None:
 
1427
            self.status = new_status
 
1428
            self.error_description = error_description
 
1429
            self.save()
 
1430
            maaslog.error(
 
1431
                "%s: Marking node failed: %s", self.hostname,
 
1432
                error_description)
 
1433
        elif is_failed_status(self.status):
 
1434
            # Silently ignore a request to fail an already failed node.
 
1435
            pass
 
1436
        else:
 
1437
            raise NodeStateViolation(
 
1438
                "The status of the node is %s; this status cannot "
 
1439
                "be transitioned to a corresponding failed status." %
 
1440
                self.status)
 
1441
 
1287
1442
    def mark_broken(self, error_description):
1288
1443
        """Mark this node as 'BROKEN'.
1289
1444
 
1290
1445
        If the node is allocated, release it first.
1291
1446
        """
1292
 
        maaslog.info("%s: Marking node broken", self.hostname)
1293
 
        if self.status == NODE_STATUS.ALLOCATED:
 
1447
        if self.status in RELEASABLE_STATUSES:
1294
1448
            self.release()
 
1449
        # release() normally sets the status to RELEASING and leaves the
 
1450
        # owner in place, override that here as we're broken.
1295
1451
        self.status = NODE_STATUS.BROKEN
 
1452
        self.owner = None
1296
1453
        self.error_description = error_description
1297
1454
        self.save()
1298
1455
 
1309
1466
    def update_power_state(self, power_state):
1310
1467
        """Update a node's power state """
1311
1468
        self.power_state = power_state
 
1469
        mark_ready = (
 
1470
            self.status == NODE_STATUS.RELEASING and
 
1471
            power_state == POWER_STATE.OFF)
 
1472
        if mark_ready:
 
1473
            # Ensure the node is fully released after a successful power
 
1474
            # down.
 
1475
            self.status = NODE_STATUS.READY
 
1476
            self.owner = None
1312
1477
        self.save()
 
1478
 
 
1479
    def claim_static_ip_addresses(self):
 
1480
        """Assign static IPs to the node's PXE MAC.
 
1481
 
 
1482
        :returns: A list of ``(ip-address, mac-address)`` tuples.
 
1483
        :raises: `StaticIPAddressExhaustion` if there are not enough IPs left.
 
1484
        """
 
1485
        mac = self.get_pxe_mac()
 
1486
 
 
1487
        if mac is None:
 
1488
            return []
 
1489
 
 
1490
        # XXX 2014-10-09 jhobbs bug=1379370
 
1491
        # It's not clear to me that this transaction needs to be here
 
1492
        # since this doesn't allocate IP addresses across multiple
 
1493
        # interfaces. This needs to be looked at some more when there is
 
1494
        # more time.
 
1495
        with transaction.atomic():
 
1496
            try:
 
1497
                static_ips = mac.claim_static_ips()
 
1498
            except StaticIPAddressTypeClash:
 
1499
                # There's already a non-AUTO IP.
 
1500
                return []
 
1501
 
 
1502
            # Return a list instead of yielding mappings as they're ready
 
1503
            # because it's all-or-nothing (hence the atomic context).
 
1504
            return [(static_ip.ip, unicode(mac)) for static_ip in static_ips]
 
1505
 
 
1506
    def get_boot_purpose(self):
 
1507
        """
 
1508
        Return a suitable "purpose" for this boot, e.g. "install".
 
1509
        """
 
1510
        # XXX: allenap bug=1031406 2012-07-31: The boot purpose is
 
1511
        # still in flux. It may be that there will just be an
 
1512
        # "ephemeral" environment and an "install" environment, and
 
1513
        # the differing behaviour between, say, enlistment and
 
1514
        # commissioning - both of which will use the "ephemeral"
 
1515
        # environment - will be governed by varying the preseed or PXE
 
1516
        # configuration.
 
1517
        if self.status in COMMISSIONING_LIKE_STATUSES:
 
1518
            # It is commissioning or disk erasing. The environment (boot
 
1519
            # images, kernel options, etc for erasing is the same as that
 
1520
            # of commissioning.
 
1521
            return "commissioning"
 
1522
        elif self.status == NODE_STATUS.DEPLOYING:
 
1523
            # Install the node if netboot is enabled,
 
1524
            # otherwise boot locally.
 
1525
            if self.netboot:
 
1526
                # Avoid circular imports.
 
1527
                from maasserver.preseed import get_preseed_type_for
 
1528
                preseed_type = get_preseed_type_for(self)
 
1529
                if preseed_type == PRESEED_TYPE.CURTIN:
 
1530
                    return "xinstall"
 
1531
                else:
 
1532
                    return "install"
 
1533
            else:
 
1534
                return "local"
 
1535
        elif self.status == NODE_STATUS.DEPLOYED:
 
1536
            return "local"
 
1537
        else:
 
1538
            return "poweroff"
 
1539
 
 
1540
    def get_pxe_mac(self):
 
1541
        """Get the MAC address this node is expected to PXE boot from.
 
1542
 
 
1543
        Normally, this will be the MAC address last used in a
 
1544
        pxeconfig() API request for the node, as recorded in the
 
1545
        'pxe_mac' property. However, if the node hasn't PXE booted since
 
1546
        the 'pxe_mac' property was added to the Node model, this will
 
1547
        return the node's first MAC address instead.
 
1548
        """
 
1549
        if self.pxe_mac is not None:
 
1550
            return self.pxe_mac
 
1551
 
 
1552
        return self.macaddress_set.first()