2418
2468
cluster_address = get_mandatory_param(request.GET, "local")
2420
2470
params = KernelParameters(
2421
arch=arch, subarch=subarch, release=series, purpose=purpose,
2422
hostname=hostname, domain=domain, preseed_url=preseed_url,
2423
log_host=server_address, fs_host=cluster_address,
2424
extra_opts=extra_kernel_opts)
2471
arch=arch, subarch=subarch, release=series, label=label,
2472
purpose=purpose, hostname=hostname, domain=domain,
2473
preseed_url=preseed_url, log_host=server_address,
2474
fs_host=cluster_address, extra_opts=extra_kernel_opts)
2426
2476
return HttpResponse(
2427
2477
json.dumps(params._asdict()),
2428
2478
content_type="application/json")
2481
def warn_if_missing_boot_images():
2482
"""Show a UI warning if any nodegroups have no boot images.
2484
This is a coarse approximation for "have boot images been imported yet?"
2485
The warning appears if any nodegroup is still completely without boot
2486
images, as is the case when the system starts up initially.
2488
nodegroup_ids_with_images = BootImage.objects.values_list(
2489
"nodegroup_id", flat=True)
2490
nodegroups_without_images = NodeGroup.objects.exclude(
2491
id__in=nodegroup_ids_with_images)
2492
nodegroups_without_images = nodegroups_without_images.filter(
2493
status=NODEGROUP_STATUS.ACCEPTED)
2494
if nodegroups_without_images.exists():
2495
accepted_clusters_url = (
2496
"%s#accepted-clusters" % absolute_reverse("settings"))
2497
warning = dedent("""\
2498
Some cluster controllers are missing boot images. Either the
2499
import task has not been initiated (for each cluster, the task
2500
must be <a href=%s>initiated by hand</a> the first time), or
2501
the import task failed.
2502
""" % quoteattr(accepted_clusters_url))
2503
register_persistent_error(COMPONENT.IMPORT_PXE_FILES, warning)
2505
discard_persistent_error(COMPONENT.IMPORT_PXE_FILES)
2508
def summarise_boot_image_object(image_object):
2509
"""Return a tuple representing identifying properties of a `BootImage`.
2511
This function has a counterpart, `summarise_boot_image_dict`. The two
2512
return the same value for the same boot image.
2514
:return: A tuple of the image's architecture, subarchitecture, release,
2518
image_object.architecture,
2519
image_object.subarchitecture,
2520
image_object.release,
2522
image_object.purpose,
2526
def summarise_boot_image_dict(image_dict):
2527
"""Return a tuple representing a reported boot-image dict.
2529
This is the counterpart to `summarise_boot_image_object`. The two return
2530
the same value for the same boot image.
2532
:return: A tuple of the image's architecture, subarchitecture, release,
2536
image_dict['architecture'],
2537
image_dict.get('subarchitecture', 'generic'),
2538
image_dict['release'],
2539
image_dict.get('label', 'release'),
2540
image_dict['purpose'],
2544
def summarise_reported_images(request):
2545
"""Return boot images reported by `request`.
2547
:param request: An http request as received by `report_boot_images`.
2548
:return: A set of tuples as produced by `summarise_boot_image_dict`.
2551
summarise_boot_image_dict(image)
2552
for image in json.loads(get_mandatory_param(request.data, 'images'))
2556
def summarise_stored_images(nodegroup):
2557
"""Return boot images for `nodegroup` as found in the database.
2559
:param nodegroup: The nodegroup whose boot images should be queried.
2560
:return: A set of tuples as produced by `summarise_boot_image_object`.
2563
summarise_boot_image_object(image)
2564
for image in BootImage.objects.filter(nodegroup=nodegroup)
2568
def store_boot_images(nodegroup, reported_images, stored_images):
2569
"""Store newly reported boot images in the database.
2571
:param nodegroup: The `NodeGroup` whose boot images are being reported.
2572
:param reported_images: Images being reported, as generated by
2573
`summarise_reported_images`.
2574
:param stored_images: Images already in the database, as generated by
2575
`summarise_stored_images`.
2577
new_images = reported_images - stored_images
2578
for arch, subarch, release, label, purpose in new_images:
2579
BootImage.objects.register_image(
2580
nodegroup=nodegroup, architecture=arch, subarchitecture=subarch,
2581
release=release, purpose=purpose, label=label)
2584
def prune_boot_images(nodegroup, reported_images, stored_images):
2585
"""Remove boot images for `nodegroup` which are no longer reported.
2587
When a nodegroup reports its boot imgaes, it reports all the images it
2588
has. Therefore, any image not being reported is obsolete. This function
2589
removes such images from the database.
2591
:param nodegroup: The `NodeGroup` whose boot images are being reported.
2592
:param reported_images: Images being reported, as generated by
2593
`summarise_reported_images`.
2594
:param stored_images: Images already in the database, as generated by
2595
`summarise_stored_images`.
2597
removed_images = stored_images - reported_images
2598
for arch, subarch, release, label, purpose in removed_images:
2599
db_images = BootImage.objects.filter(
2600
architecture=arch, subarchitecture=subarch,
2601
release=release, label=label, purpose=purpose)
2605
DISPLAYED_BOOTIMAGE_FIELDS = (
2615
class BootImageHandler(OperationsHandler):
2616
"""Manage a boot image."""
2617
api_doc_section_name = "Boot image"
2618
create = replace = update = delete = None
2621
fields = DISPLAYED_BOOTIMAGE_FIELDS
2623
def read(self, request, uuid, id):
2624
"""Read a boot image."""
2625
nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
2626
return get_object_or_404(
2627
BootImage, nodegroup=nodegroup, id=id)
2630
def resource_uri(cls, bootimage=None):
2631
if bootimage is None:
2636
uuid = bootimage.nodegroup.uuid
2637
return ('boot_image_handler', (uuid, id))
2431
2640
class BootImagesHandler(OperationsHandler):
2641
"""Manage the collection of boot images."""
2642
api_doc_section_name = "Boot images"
2433
2644
create = replace = update = delete = None
2436
def resource_uri(cls):
2437
return ('boot_images_handler', [])
2647
def resource_uri(cls, nodegroup=None):
2648
if nodegroup is None:
2651
uuid = nodegroup.uuid
2652
return ('boot_images_handler', [uuid])
2654
def read(self, request, uuid):
2655
"""List boot images.
2657
Get a listing of a cluster's boot images.
2659
:param uuid: The UUID of the cluster for which the images
2662
nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
2663
return BootImage.objects.filter(nodegroup=nodegroup)
2439
2665
@operation(idempotent=False)
2440
def report_boot_images(self, request):
2666
def report_boot_images(self, request, uuid):
2441
2667
"""Report images available to net-boot nodes from.
2669
:param uuid: The UUID of the cluster for which the images are
2443
2671
:param images: A list of dicts, each describing a boot image with
2444
2672
these properties: `architecture`, `subarchitecture`, `release`,
2445
`purpose`, all as in the code that determines TFTP paths for
2673
`purpose`, and optionally, `label` (which defaults to "release").
2674
These should match the code that determines TFTP paths for these
2448
nodegroup_uuid = get_mandatory_param(request.data, "nodegroup")
2449
nodegroup = get_object_or_404(NodeGroup, uuid=nodegroup_uuid)
2677
nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
2450
2678
check_nodegroup_access(request, nodegroup)
2451
images = json.loads(get_mandatory_param(request.data, 'images'))
2453
for image in images:
2454
BootImage.objects.register_image(
2455
nodegroup=nodegroup,
2456
architecture=image['architecture'],
2457
subarchitecture=image.get('subarchitecture', 'generic'),
2458
release=image['release'],
2459
purpose=image['purpose'])
2461
# Work out if any nodegroups are missing images.
2462
nodegroup_ids_with_images = BootImage.objects.values_list(
2463
"nodegroup_id", flat=True)
2464
nodegroups_missing_images = NodeGroup.objects.exclude(
2465
id__in=nodegroup_ids_with_images)
2466
nodegroups_missing_images = nodegroups_missing_images.filter(
2467
status=NODEGROUP_STATUS.ACCEPTED)
2468
if nodegroups_missing_images.exists():
2469
accepted_clusters_url = (
2470
"%s#accepted-clusters" % absolute_reverse("settings"))
2471
warning = dedent("""\
2472
Some cluster controllers are missing boot images. Either the
2473
import task has not been initiated (for each cluster, the task
2474
must be <a href=%s>initiated by hand</a> the first time), or
2475
the import task failed.
2476
""" % quoteattr(accepted_clusters_url))
2477
register_persistent_error(COMPONENT.IMPORT_PXE_FILES, warning)
2479
discard_persistent_error(COMPONENT.IMPORT_PXE_FILES)
2679
reported_images = summarise_reported_images(request)
2680
existing_images = summarise_stored_images(nodegroup)
2681
store_boot_images(nodegroup, reported_images, existing_images)
2682
prune_boot_images(nodegroup, reported_images, existing_images)
2683
warn_if_missing_boot_images()
2481
2684
return HttpResponse("OK")
2784
2995
@operation(idempotent=False)
2785
def connect_nodes(self, request, name):
2786
"""Connect the given nodes to this network.
2788
Connecting a node to a network which it is already connected to does
2791
:param nodes: A list of `system_id` identifiers for nodes.
2996
def connect_macs(self, request, name):
2997
"""Connect the given MAC addresses to this network.
2999
These MAC addresses must belong to nodes in the MAAS, and have been
3000
registered as such in MAAS.
3002
Connecting a network interface to a network which it is already
3003
connected to does nothing.
3005
:param macs: A list of node MAC addresses, in text form.
2793
3007
network = get_object_or_404(Network, name=name)
2794
form = NetworkConnectNodesForm(data=request.data)
3008
form = NetworkConnectMACsForm(network=network, data=request.data)
2795
3009
if not form.is_valid():
2796
3010
raise ValidationError(form.errors)
2797
network.node_set.add(*form.get_nodes())
2800
3014
@operation(idempotent=False)
2801
def disconnect_nodes(self, request, name):
2802
"""Disconnect the given nodes from this network.
2804
Removing a node from a network which it is not connected to does
2807
:param nodes: A list of `system_id` identifiers for nodes.
3015
def disconnect_macs(self, request, name):
3016
"""Disconnect the given MAC addresses from this network.
3018
Removing a MAC address from a network which it is not connected to
3021
:param macs: A list of node MAC addresses, in text form.
2809
3023
network = get_object_or_404(Network, name=name)
2810
form = NetworkConnectNodesForm(data=request.data)
3024
form = NetworkDisconnectMACsForm(network=network, data=request.data)
2811
3025
if not form.is_valid():
2812
3026
raise ValidationError(form.errors)
2813
network.node_set.remove(*form.get_nodes())
2815
3029
@operation(idempotent=True)
2816
def list_connected_nodes(self, request, name):
2817
"""Returns the list of nodes connected to this network.
3030
def list_connected_macs(self, request, name):
3031
"""Returns the list of MAC addresses connected to this network.
2819
Only the nodes visible to the requesting user are returned.
3033
Only MAC addresses for nodes visible to the requesting user are
2821
3036
network = get_object_or_404(Network, name=name)
2822
return Node.objects.get_nodes(
3037
visible_nodes = Node.objects.get_nodes(
2823
3038
request.user, NODE_PERMISSION.VIEW,
2824
from_nodes=network.node_set.all())
3039
from_nodes=Node.objects.all())
3040
return network.macaddress_set.filter(node__in=visible_nodes).order_by(
3041
'node__hostname', 'mac_address')
2827
3044
def resource_uri(cls, network=None):