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 (
74
StaticIPAddressExhaustion,
89
from maasserver.models.macaddress import (
91
update_mac_cluster_interfaces,
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,
103
from maasserver.rpc import getClientFor
79
104
from maasserver.utils import (
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,
111
from provisioningserver.power.poweraction import UnknownPowerType
112
from provisioningserver.rpc.cluster import (
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
96
124
maaslog = get_maas_logger("node")
127
def wait_for_power_commands(deferreds):
128
"""Wait for a collection of power command deferreds to return or fail.
130
:param deferreds: A collection of deferreds upon which to wait.
131
:raises: MultipleFailures if any of the deferreds fail.
133
@asynchronous(timeout=30)
134
def block_until_commands_complete():
135
return DeferredList(deferreds, consumeErrors=True)
137
results = block_until_commands_complete()
140
result for success, result in results if not success)
142
if len(failures) != 0:
143
raise MultipleFailures(*failures)
99
146
def generate_node_system_id():
100
147
return 'node-%s' % uuid1()
103
# Information about valid node status transitions.
121
NODE_STATUS.COMMISSIONING,
127
NODE_STATUS.COMMISSIONING: [
128
NODE_STATUS.FAILED_TESTS,
135
NODE_STATUS.FAILED_TESTS: [
136
NODE_STATUS.COMMISSIONING,
142
NODE_STATUS.COMMISSIONING,
143
NODE_STATUS.ALLOCATED,
144
NODE_STATUS.RESERVED,
149
NODE_STATUS.RESERVED: [
151
NODE_STATUS.ALLOCATED,
156
NODE_STATUS.ALLOCATED: [
162
NODE_STATUS.MISSING: [
165
NODE_STATUS.ALLOCATED,
166
NODE_STATUS.COMMISSIONING,
169
NODE_STATUS.RETIRED: [
175
NODE_STATUS.BROKEN: [
176
NODE_STATUS.COMMISSIONING,
182
class UnknownPowerType(Exception):
183
"""Raised when a node has an unknown power type."""
186
150
def validate_hostname(hostname):
187
151
"""Validator for hostnames.
370
340
:return: Those Nodes for which shutdown was actually requested.
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)
377
power_params = node.get_effective_power_parameters()
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.
384
"%s: Node has an unknown power type. Not creating "
385
"power down event.", node.hostname)
387
power_params['power_off_mode'] = stop_mode
388
# WAKE_ON_LAN does not support poweroff.
389
if node_power_type != 'ether_wake':
391
"%s: Asking cluster to power off node", node.hostname)
392
power_off.apply_async(
393
queue=node.work_queue, args=[node_power_type],
395
processed_nodes.append(node)
396
return processed_nodes
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):
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
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))
361
system_id for system_id, _, _, _ in nodes_stop_info]
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)
368
# Return a list of those nodes that we've sent power commands for.
370
node for node in nodes if node.system_id in powered_systems)
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.
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
396
:raises: `StaticIPAddressExhaustion` if there are not enough IP
397
addresses left in the static range..
418
maaslog.debug("Starting nodes: %s", ids)
419
399
# Avoid circular imports.
420
400
from metadataserver.models import NodeUserData
402
# Obtain node model objects for each node specified.
422
403
nodes = self.get_nodes(by_user, NODE_PERMISSION.EDIT, ids=ids)
424
NodeUserData.objects.set_user_data(node, user_data)
427
maaslog.info("%s: Attempting start up", node.hostname)
428
power_params = node.get_effective_power_parameters()
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.
435
"%s: Node has an unknown power type. Not creating "
436
"power up event.", node.hostname)
438
if node_power_type == 'ether_wake':
439
mac = power_params.get('mac_address')
440
do_start = (mac != '' and mac is not None)
446
if node.status == NODE_STATUS.ALLOCATED:
447
tasks.extend(node.claim_static_ips())
448
except StaticIPAddressExhaustion:
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.
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.
464
task = power_on.si(node_power_type, **power_params)
465
task.set(queue=node.work_queue)
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
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)
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)
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()
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.
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)
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})
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):
442
power_info = node.get_effective_power_info()
443
if power_info.can_be_started:
444
yield node, power_info
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))
451
system_id for system_id, _, _, _ in nodes_start_info]
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)
458
# Return a list of those nodes that we've sent power commands for.
460
node for node in nodes if node.system_id in powered_systems)
474
463
def patch_pgarray_types():
661
672
return nodegroup_fqdn(self.hostname, self.nodegroup.name)
662
673
return self.hostname
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.
669
Each MAC on the node that is connected to a managed cluster
670
interface will get an IP.
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
677
tasks = self._create_tasks_for_static_ips()
678
except StaticIPAddressExhaustion:
679
StaticIPAddress.objects.deallocate_by_node(self)
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])
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',
701
dhcp_task.set(queue=self.work_queue)
704
def _create_tasks_for_static_ips(self):
706
# Get a new AUTO static IP for each MAC on a managed interface.
707
macs = self.mac_addresses_on_managed_interfaces()
710
sips = mac.claim_static_ips()
711
except StaticIPAddressTypeClash:
712
# There's already a non-AUTO IP, so nothing to do.
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
719
tasks.append(self._create_hostmap_task(mac, sip))
721
"%s: Claimed static IP %s on %s", self.hostname,
722
sip.ip, mac.mac_address.get_raw())
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)
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()
681
def start_deployment(self):
682
"""Mark a node as being deployed."""
683
self.status = NODE_STATUS.DEPLOYING
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.
689
deployment_time = self.get_deployment_time()
690
self.start_transition_monitor(deployment_time)
692
def end_deployment(self):
693
"""Mark a node as successfully deployed."""
694
self.status = NODE_STATUS.DEPLOYED
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.
701
def start_transition_monitor(self, timeout):
702
"""Start cluster-side transition monitor."""
704
'node_status': self.status,
707
deadline = datetime.now(tz=amp.utc) + timedelta(seconds=timeout)
709
'deadline': deadline,
710
'id': self.system_id,
713
client = getClientFor(self.nodegroup.uuid)
714
call = client(StartMonitors, monitors=monitors)
717
except crochet.TimeoutError as error:
719
"%s: Unable to start transition monitor: %s",
720
self.hostname, error)
721
maaslog.info("%s: Starting monitor: %s", self.hostname, monitors[0])
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)
729
except crochet.TimeoutError as 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)
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'])
741
"Node operation '%s' timed out after %s." % (
743
NODE_STATUS_CHOICES_DICT[self.status],
734
747
def ip_addresses(self):
735
748
"""IP addresses allocated to this node.
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
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.
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
930
# The commissioning profile is handled in start_nodes.
932
"%s: Starting commissioning for", self.hostname)
933
Node.objects.start_nodes(
934
[self.system_id], user, user_data=commissioning_user_data)
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:
979
"%s: Unable to start node: %s",
980
self.hostname, unicode(ex))
981
self.status = old_status
984
# Let the exception bubble up, since the UI or API will have to
988
maaslog.info("%s: Commissioning started", self.hostname)
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]))
944
998
"%s: Aborting commissioning", self.hostname)
945
stopped_node = Node.objects.stop_nodes([self.system_id], user)
946
if len(stopped_node) == 1:
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:
1006
"%s: Unable to shut node down: %s",
1007
self.hostname, unicode(ex))
947
1010
self.status = NODE_STATUS.NEW
1012
maaslog.info("%s: Commissioning aborted", self.hostname)
950
1014
def delete(self):
1015
"""Delete this node.
1017
:raises MultipleFailures: If host maps cannot be deleted.
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]))
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()
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)
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()
971
1047
super(Node, self).delete()
973
def delete_static_host_maps(self, for_ips):
974
"""Delete any host maps for static IPs allocated to this node.
976
:param for_ips: Delete the maps for these IP addresses only.
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)
987
"%s: Asking cluster to delete static host maps", self.hostname)
988
chain = celery.chain(tasks)
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.
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.
999
Return None if there is nothing to delete.
1001
nodegroup = self.nodegroup
1002
if len(nodegroup.get_managed_interfaces()) == 0:
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))
1013
for lease in leases:
1014
# XXX See bug 1039362 regarding server_address
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)
1023
return celery.chain(tasks)
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.
1036
def _delete_dynamic_host_maps(self):
1037
"""If any DHCPLeases exist for this node, remove any associated
1039
chain = self._build_dynamic_host_map_deletion_task()
1040
if chain is not None:
1049
def delete_host_maps(self, for_ips):
1050
"""Delete any host maps for IPs allocated to this node.
1052
This should probably live on `NodeGroup`.
1054
:param for_ips: The set of IP addresses to remove host maps for.
1055
:type for_ips: `set`
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
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)
1043
1071
def set_random_hostname(self):
1044
1072
"""Set `hostname` from a shuffled list of candidate names.
1240
1274
maaslog.info("%s allocated to user %s", self.hostname, user.username)
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
1281
disk_erase_user_data = generate_user_data(node=self)
1283
"%s: Starting disk erasure", self.hostname)
1284
# Change the status of the node now to avoid races when starting
1286
self.status = NODE_STATUS.DISK_ERASING
1288
transaction.commit()
1290
Node.objects.start_nodes(
1291
[self.system_id], user, user_data=disk_erase_user_data)
1292
except Exception as ex:
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
1302
transaction.commit()
1306
"%s: Disk erasure started.", self.hostname)
1308
def abort_disk_erasing(self, user):
1310
Power off disk erasing node and set its status to 'failed disk
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]))
1319
"%s: Aborting disk erasing", self.hostname)
1321
Node.objects.stop_nodes([self.system_id], user)
1322
except Exception as ex:
1324
"%s: Unable to shut node down: %s",
1325
self.hostname, unicode(ex))
1328
self.status = NODE_STATUS.FAILED_DISK_ERASING
1331
def abort_operation(self, user):
1332
"""Abort the current operation.
1333
This currently only supports aborting Disk Erasing.
1335
if self.status == NODE_STATUS.DISK_ERASING:
1336
self.abort_disk_erasing(user)
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]))
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.
1347
:raises MultipleFailures: If host maps cannot be deleted.
1244
1349
maaslog.info("%s: Releasing node", self.hostname)
1245
Node.objects.stop_nodes([self.system_id], self.owner)
1351
Node.objects.stop_nodes([self.system_id], self.owner)
1352
except Exception as ex:
1354
"%s: Unable to shut node down: %s", self.hostname,
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
1255
if self.status != NODE_STATUS.BROKEN:
1256
self.status = NODE_STATUS.READY
1362
if self.power_state == POWER_STATE.OFF:
1363
# Node is already off.
1364
self.status = NODE_STATUS.READY
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
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
1258
1376
self.token = None
1259
1377
self.agent_name = ''
1260
1378
self.set_netboot()
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:
1276
if self.owner is None:
1408
NODE_STATUS.DEPLOYED: "Deployed",
1409
NODE_STATUS.DEPLOYING: "Deploying",
1410
NODE_STATUS.FAILED_DEPLOYMENT: "Failed deployment",
1412
return mapping.get(self.status, "Not in deployment")
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)
1419
def mark_failed(self, error_description):
1420
"""Mark this node as failed.
1422
The actual 'failed' state depends on the current status of the
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
1431
"%s: Marking node failed: %s", self.hostname,
1433
elif is_failed_status(self.status):
1434
# Silently ignore a request to fail an already failed node.
1437
raise NodeStateViolation(
1438
"The status of the node is %s; this status cannot "
1439
"be transitioned to a corresponding failed status." %
1287
1442
def mark_broken(self, error_description):
1288
1443
"""Mark this node as 'BROKEN'.
1290
1445
If the node is allocated, release it first.
1292
maaslog.info("%s: Marking node broken", self.hostname)
1293
if self.status == NODE_STATUS.ALLOCATED:
1447
if self.status in RELEASABLE_STATUSES:
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
1296
1453
self.error_description = error_description
1309
1466
def update_power_state(self, power_state):
1310
1467
"""Update a node's power state """
1311
1468
self.power_state = power_state
1470
self.status == NODE_STATUS.RELEASING and
1471
power_state == POWER_STATE.OFF)
1473
# Ensure the node is fully released after a successful power
1475
self.status = NODE_STATUS.READY
1479
def claim_static_ip_addresses(self):
1480
"""Assign static IPs to the node's PXE MAC.
1482
:returns: A list of ``(ip-address, mac-address)`` tuples.
1483
:raises: `StaticIPAddressExhaustion` if there are not enough IPs left.
1485
mac = self.get_pxe_mac()
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
1495
with transaction.atomic():
1497
static_ips = mac.claim_static_ips()
1498
except StaticIPAddressTypeClash:
1499
# There's already a non-AUTO IP.
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]
1506
def get_boot_purpose(self):
1508
Return a suitable "purpose" for this boot, e.g. "install".
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
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
1521
return "commissioning"
1522
elif self.status == NODE_STATUS.DEPLOYING:
1523
# Install the node if netboot is enabled,
1524
# otherwise boot locally.
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:
1535
elif self.status == NODE_STATUS.DEPLOYED:
1540
def get_pxe_mac(self):
1541
"""Get the MAC address this node is expected to PXE boot from.
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.
1549
if self.pxe_mac is not None:
1552
return self.macaddress_set.first()