~lutostag/ubuntu/trusty/maas/1.5.2+packagefix

« back to all changes in this revision

Viewing changes to src/maasserver/api.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez
  • Date: 2014-03-28 10:43:53 UTC
  • mto: This revision was merged to the branch mainline in revision 57.
  • Revision ID: package-import@ubuntu.com-20140328104353-ekpolg0pm5xnvq2s
Tags: upstream-1.5+bzr2204
ImportĀ upstreamĀ versionĀ 1.5+bzr2204

Show diffs side-by-side

added added

removed removed

Lines of Context:
61
61
    "AnonNodesHandler",
62
62
    "api_doc",
63
63
    "api_doc_title",
 
64
    "BootImageHandler",
64
65
    "BootImagesHandler",
65
66
    "CommissioningScriptHandler",
66
67
    "CommissioningScriptsHandler",
142
143
    get_mandatory_param,
143
144
    get_oauth_token,
144
145
    get_optional_list,
 
146
    get_optional_param,
145
147
    )
146
148
from maasserver.apidoc import (
147
149
    describe_resource,
148
150
    find_api_resources,
149
151
    generate_api_docs,
150
152
    )
 
153
from maasserver.clusterrpc.power_parameters import (
 
154
    get_all_power_types_from_clusters,
 
155
    get_power_types,
 
156
    )
151
157
from maasserver.components import (
152
158
    discard_persistent_error,
153
159
    register_persistent_error,
154
160
    )
155
161
from maasserver.enum import (
156
 
    ARCHITECTURE,
157
162
    COMPONENT,
158
163
    NODE_PERMISSION,
159
164
    NODE_STATUS,
176
181
    get_action_form,
177
182
    get_node_create_form,
178
183
    get_node_edit_form,
179
 
    NetworkConnectNodesForm,
 
184
    NetworkConnectMACsForm,
 
185
    NetworkDisconnectMACsForm,
180
186
    NetworkForm,
181
 
    NetworkListForm,
182
187
    NetworksListingForm,
183
188
    NodeActionForm,
184
189
    NodeGroupEdit,
238
243
from piston.emitters import JSONEmitter
239
244
from piston.handler import typemapper
240
245
from piston.utils import rc
241
 
from provisioningserver.enum import (
242
 
    get_power_types,
243
 
    UNKNOWN_POWER_TYPE,
244
 
    )
245
246
from provisioningserver.kernel_opts import KernelParameters
 
247
from provisioningserver.power_schema import UNKNOWN_POWER_TYPE
246
248
import simplejson as json
247
249
 
248
250
# Node's fields exposed on the API.
261
263
    'tag_names',
262
264
    'ip_addresses',
263
265
    'routers',
 
266
    'zone',
264
267
    )
265
268
 
266
269
 
273
276
    if power_type is None:
274
277
        return
275
278
 
276
 
    if power_type in get_power_types() or power_type == UNKNOWN_POWER_TYPE:
 
279
    power_types = get_power_types([node.nodegroup])
 
280
 
 
281
    if power_type in power_types or power_type == UNKNOWN_POWER_TYPE:
277
282
        node.power_type = power_type
278
283
    else:
279
284
        raise MAASAPIBadRequest("Bad power_type '%s'" % power_type)
293
298
 
294
299
    The Node is identified by its system_id.
295
300
    """
 
301
    api_doc_section_name = "Node"
 
302
 
296
303
    create = None  # Disable create.
297
304
    model = Node
298
305
    fields = DISPLAYED_NODE_FIELDS
321
328
 
322
329
        :param hostname: The new hostname for this node.
323
330
        :type hostname: unicode
324
 
        :param architecture: The new architecture for this node (see
325
 
            vocabulary `ARCHITECTURE`).
 
331
        :param architecture: The new architecture for this node.
326
332
        :type architecture: unicode
327
 
        :param power_type: The new power type for this node (see
328
 
            vocabulary `POWER_TYPE`).  Note that if you set power_type to
329
 
            use the default value, power_parameters will be set to the empty
330
 
            string.  Available to admin users.
 
333
        :param power_type: The new power type for this node. If you use the
 
334
            default value, power_parameters will be set to the empty string.
 
335
            Available to admin users.
331
336
        :type power_type: unicode
332
337
        :param power_parameters_{param1}: The new value for the 'param1'
333
338
            power parameter.  Note that this is dynamic as the available
341
346
            parameters for this node should be checked against the expected
342
347
            power parameters for the node's power type ('true' or 'false').
343
348
            The default is 'false'.
344
 
        :type power_parameters_skip_validation: unicode
 
349
        :type power_parameters_skip_check: unicode
 
350
        :param zone: Name of a valid physical zone in which to place this node
 
351
        :type zone: unicode
345
352
        """
346
353
        node = Node.objects.get_node_or_404(
347
354
            system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
348
355
        Form = get_node_edit_form(request.user)
349
 
        form = Form(request.data, instance=node)
 
356
        form = Form(data=request.data, instance=node)
 
357
 
350
358
        if form.is_valid():
351
359
            return form.save()
352
360
        else:
469
477
        probe_details = get_single_probed_details(node.system_id)
470
478
        probe_details_report = {
471
479
            name: None if data is None else bson.Binary(data)
472
 
            for name, data in probe_details.iteritems()
 
480
            for name, data in probe_details.items()
473
481
        }
474
482
        return HttpResponse(
475
483
            bson.BSON.encode(probe_details_report),
476
484
            # Not sure what media type to use here.
477
485
            content_type='application/bson')
478
486
 
479
 
    @operation(idempotent=True)
480
 
    def list_connected_networks(self, request, system_id):
481
 
        """Returns the list of networks connected to this node."""
482
 
        node = get_object_or_404(Node, system_id=system_id)
483
 
        return node.networks.all().order_by('name')
484
 
 
485
 
    @admin_method
486
 
    @operation(idempotent=False)
487
 
    def connect_networks(self, request, system_id):
488
 
        """Connect the given networks to this node.
489
 
 
490
 
        :param networks: A list of `name` identifiers for networks.
491
 
        """
492
 
        node = get_object_or_404(Node, system_id=system_id)
493
 
        form = NetworkListForm(data=request.data)
494
 
        if not form.is_valid():
495
 
            raise ValidationError(form.errors)
496
 
        node.networks.add(*form.get_networks())
497
 
 
498
 
    @admin_method
499
 
    @operation(idempotent=False)
500
 
    def disconnect_networks(self, request, system_id):
501
 
        """Disconnect the given networks from this node.
502
 
 
503
 
        :param networks: A list of `name` identifiers for networks.
504
 
        """
505
 
        node = get_object_or_404(Node, system_id=system_id)
506
 
        form = NetworkListForm(data=request.data)
507
 
        if not form.is_valid():
508
 
            raise ValidationError(form.errors)
509
 
        node.networks.remove(*form.get_networks())
510
 
 
511
487
 
512
488
def create_node(request):
513
489
    """Service an http request to create a node.
561
537
            altered_query_data['nodegroup'] = nodegroup
562
538
 
563
539
    Form = get_node_create_form(request.user)
564
 
    form = Form(altered_query_data)
 
540
    form = Form(data=altered_query_data)
565
541
    if form.is_valid():
566
542
        node = form.save()
567
543
        # Hack in the power parameters here.
625
601
 
626
602
 
627
603
class NodesHandler(OperationsHandler):
628
 
    """Manage the collection of all Nodes in the MAAS."""
 
604
    """Manage the collection of all the nodes in the MAAS."""
 
605
    api_doc_section_name = "Nodes"
629
606
    create = read = update = delete = None
630
607
    anonymous = AnonNodesHandler
631
608
 
945
922
 
946
923
    The Node is identified by its system_id.
947
924
    """
 
925
    api_doc_section_name = "Node MAC addresses"
948
926
    update = delete = None
949
927
 
950
928
    def read(self, request, system_id):
967
945
 
968
946
 
969
947
class NodeMacHandler(OperationsHandler):
970
 
    """Manage a MAC address.
 
948
    """Manage a Node MAC address.
971
949
 
972
950
    The MAC address object is identified by the system_id for the Node it
973
951
    is attached to, plus the MAC address itself.
974
952
    """
 
953
    api_doc_section_name = "Node MAC address"
975
954
    create = update = None
976
955
    fields = ('mac_address',)
977
956
    model = MACAddress
1086
1065
 
1087
1066
 
1088
1067
class SSHKeysHandler(OperationsHandler):
1089
 
    """Operations on multiple keys."""
 
1068
    """Manage the collection of all the SSH keys in this MAAS."""
 
1069
    api_doc_section_name = "SSH Keys"
 
1070
 
1090
1071
    create = read = update = delete = None
1091
1072
 
1092
1073
    @operation(idempotent=True)
1123
1104
 
1124
1105
    SSH keys can be retrieved or deleted.
1125
1106
    """
 
1107
    api_doc_section_name = "SSH Key"
 
1108
 
1126
1109
    fields = DISPLAY_SSHKEY_FIELDS
1127
1110
    model = SSHKey
1128
1111
    create = update = None
1155
1138
 
1156
1139
    The file is identified by its filename and owner.
1157
1140
    """
 
1141
    api_doc_section_name = "File"
1158
1142
    model = FileStorage
1159
1143
    fields = DISPLAYED_FILES_FIELDS
1160
1144
    create = update = None
1196
1180
 
1197
1181
 
1198
1182
class FilesHandler(OperationsHandler):
1199
 
    """File management operations."""
 
1183
    """Manage the collection of all the files in this MAAS."""
 
1184
    api_doc_section_name = "Files"
1200
1185
    create = read = update = delete = None
1201
1186
    anonymous = AnonFilesHandler
1202
1187
 
1413
1398
 
1414
1399
 
1415
1400
class NodeGroupsHandler(OperationsHandler):
1416
 
    """Manage NodeGroups."""
 
1401
    """Manage the collection of all the nodegroups in this MAAS."""
 
1402
 
 
1403
    api_doc_section_name = "Nodegroups"
1417
1404
    anonymous = AnonNodeGroupsHandler
1418
1405
    create = read = update = delete = None
1419
1406
    fields = DISPLAYED_NODEGROUP_FIELDS
1420
1407
 
1421
1408
    @operation(idempotent=True)
1422
1409
    def list(self, request):
1423
 
        """List of node groups."""
 
1410
        """List nodegroups."""
1424
1411
        return NodeGroup.objects.all()
1425
1412
 
1426
1413
    @admin_method
1448
1435
            "Import of boot images started on all cluster controllers",
1449
1436
            status=httplib.OK)
1450
1437
 
 
1438
    @operation(idempotent=True)
 
1439
    def describe_power_types(self, request):
 
1440
        """Query all the cluster controllers for power information.
 
1441
 
 
1442
        :return: a list of dicts that describe the power types in this format.
 
1443
        """
 
1444
        return get_all_power_types_from_clusters()
 
1445
 
1451
1446
    @admin_method
1452
1447
    @operation(idempotent=False)
1453
1448
    def reject(self, request):
1498
1493
 
1499
1494
    Each NodeGroup has its own uuid.
1500
1495
    """
 
1496
    api_doc_section_name = "Nodegroup"
1501
1497
 
1502
1498
    create = delete = None
1503
1499
    fields = DISPLAYED_NODEGROUP_FIELDS
1700
1696
        :type username: unicode
1701
1697
        :param password: The password for the chassis.
1702
1698
        :type password: unicode
 
1699
 
 
1700
        The following are optional if you are probing a seamicro15k:
 
1701
 
 
1702
        :param power_control: The power_control to use, either ipmi (default)
 
1703
            or restapi.
 
1704
        :type power_control: unicode
1703
1705
        """
1704
1706
        nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
1705
1707
 
1706
 
        model = get_mandatory_param(request.data, 'mode')
 
1708
        model = get_mandatory_param(request.data, 'model')
1707
1709
        if model == 'seamicro15k':
1708
1710
            mac = get_mandatory_param(request.data, 'mac')
1709
1711
            username = get_mandatory_param(request.data, 'username')
1710
1712
            password = get_mandatory_param(request.data, 'password')
 
1713
            power_control = get_optional_param(
 
1714
                request.data, 'power_control', default='ipmi',
 
1715
                validator=validators.OneOf(['impi', 'restapi', 'restapi2']))
1711
1716
 
1712
 
            nodegroup.add_seamicro15k(mac, username, password)
 
1717
            nodegroup.add_seamicro15k(
 
1718
                mac, username, password, power_control=power_control)
1713
1719
        else:
1714
1720
            return HttpResponse(status=httplib.BAD_REQUEST)
1715
1721
 
1721
1727
 
1722
1728
 
1723
1729
class NodeGroupInterfacesHandler(OperationsHandler):
1724
 
    """Manage NodeGroupInterfaces.
 
1730
    """Manage the collection of all the NodeGroupInterfaces in this MAAS.
1725
1731
 
1726
1732
    A NodeGroupInterface is a network interface attached to a cluster
1727
1733
    controller, with its network properties.
1728
1734
    """
 
1735
    api_doc_section_name = "Nodegroup interfaces"
 
1736
 
1729
1737
    create = read = update = delete = None
1730
1738
    fields = DISPLAYED_NODEGROUPINTERFACE_FIELDS
1731
1739
 
1783
1791
    A NodeGroupInterface is identified by the uuid for its NodeGroup, and
1784
1792
    the name of the network interface it represents: "eth0" for example.
1785
1793
    """
 
1794
    api_doc_section_name = "Nodegroup interface"
 
1795
 
1786
1796
    create = delete = None
1787
1797
    fields = DISPLAYED_NODEGROUPINTERFACE_FIELDS
1788
1798
 
1872
1882
 
1873
1883
class AccountHandler(OperationsHandler):
1874
1884
    """Manage the current logged-in user."""
 
1885
    api_doc_section_name = "Logged-in user"
1875
1886
    create = read = update = delete = None
1876
1887
 
1877
1888
    @operation(idempotent=False)
1910
1921
 
1911
1922
 
1912
1923
class TagHandler(OperationsHandler):
1913
 
    """Manage individual Tags.
 
1924
    """Manage a Tag.
1914
1925
 
1915
1926
    Tags are properties that can be associated with a Node and serve as
1916
1927
    criteria for selecting and allocating nodes.
1917
1928
 
1918
1929
    A Tag is identified by its name.
1919
1930
    """
 
1931
    api_doc_section_name = "Tag"
1920
1932
    create = None
1921
1933
    model = Tag
1922
1934
    fields = (
2042
2054
 
2043
2055
 
2044
2056
class TagsHandler(OperationsHandler):
2045
 
    """Manage collection of Tags."""
 
2057
    """Manage the collection of all the Tags in this MAAS."""
 
2058
    api_doc_section_name = "Tags"
2046
2059
    create = read = update = delete = None
2047
2060
 
2048
2061
    @operation(idempotent=False)
2084
2097
 
2085
2098
class MaasHandler(OperationsHandler):
2086
2099
    """Manage the MAAS server."""
 
2100
    api_doc_section_name = "MAAS server"
2087
2101
    create = read = update = delete = None
2088
2102
 
2089
2103
    @operation(idempotent=False)
2134
2148
 
2135
2149
 
2136
2150
class UsersHandler(OperationsHandler):
2137
 
    """API for user accounts."""
 
2151
    """Manage the user accounts of this MAAS."""
 
2152
    api_doc_section_name = "Users"
2138
2153
    update = delete = None
2139
2154
 
2140
2155
    @classmethod
2177
2192
 
2178
2193
 
2179
2194
class UserHandler(OperationsHandler):
2180
 
    """API for a user account."""
 
2195
    """Manage a user account."""
 
2196
    api_doc_section_name = "User"
2181
2197
    create = update = delete = None
2182
2198
 
2183
2199
    model = User
2191
2207
        return get_object_or_404(User, username=username)
2192
2208
 
2193
2209
 
 
2210
# MAAS capabilities. See docs/capabilities.rst for documentation.
 
2211
CAP_NETWORKS_MANAGEMENT = 'networks-management'
 
2212
 
 
2213
API_CAPABILITIES_LIST = [
 
2214
    CAP_NETWORKS_MANAGEMENT,
 
2215
]
 
2216
 
 
2217
 
 
2218
class VersionHandler(AnonymousOperationsHandler):
 
2219
    """Information about this MAAS instance.
 
2220
 
 
2221
    This returns a JSON dictionary with information about this
 
2222
    MAAS instance.
 
2223
    {
 
2224
        'capabilities': ['capability1', 'capability2', ...]
 
2225
    }
 
2226
    """
 
2227
    api_doc_section_name = "MAAS version"
 
2228
    create = update = delete = None
 
2229
 
 
2230
    def read(self, request):
 
2231
        version_info = {
 
2232
            'capabilities': API_CAPABILITIES_LIST,
 
2233
        }
 
2234
        return HttpResponse(
 
2235
            version_info, mimetype='application/json; charset=utf-8',
 
2236
            status=httplib.OK)
 
2237
 
 
2238
 
2194
2239
# Title section for the API documentation.  Matches in style, format,
2195
2240
# etc. whatever render_api_docs() produces, so that you can concatenate
2196
2241
# the two.
2229
2274
    for doc in generate_api_docs(resources):
2230
2275
        uri_template = doc.resource_uri_template
2231
2276
        exports = doc.handler.exports.items()
 
2277
        # Derive a section title from the name of the handler class.
 
2278
        section_name = doc.handler.api_doc_section_name
 
2279
        line(section_name)
 
2280
        line('=' * len(section_name))
 
2281
        line(doc.handler.__doc__)
 
2282
        line()
 
2283
        line()
2232
2284
        for (http_method, operation), function in sorted(exports):
2233
2285
            line("``%s %s``" % (http_method, uri_template), end="")
2234
2286
            if operation is not None:
2367
2419
        hostname = 'maas-enlist'
2368
2420
        domain = Config.objects.get_config('enlistment_domain')
2369
2421
 
2370
 
        try:
2371
 
            pxelinux_arch = request.GET['arch']
2372
 
        except KeyError:
 
2422
        arch = get_optional_param(request.GET, 'arch')
 
2423
        if arch is None:
2373
2424
            if 'mac' in request.GET:
2374
2425
                # Request was pxelinux.cfg/01-<mac>, so attempt fall back
2375
2426
                # to pxelinux.cfg/default-<arch>-<subarch> for arch detection.
2381
2432
                image = BootImage.objects.get_default_arch_image_in_nodegroup(
2382
2433
                    nodegroup, series, purpose=purpose)
2383
2434
                if image is None:
2384
 
                    arch = ARCHITECTURE.i386.split('/')[0]
 
2435
                    arch = 'i386'
2385
2436
                else:
2386
2437
                    arch = image.architecture
2387
 
        else:
2388
 
            # Map from pxelinux namespace architecture names to MAAS namespace
2389
 
            # architecture names. If this gets bigger, an external lookup table
2390
 
            # would make sense. But here is fine for something as trivial as it
2391
 
            # is right now.
2392
 
            if pxelinux_arch == 'arm':
2393
 
                arch = 'armhf'
2394
 
            else:
2395
 
                arch = pxelinux_arch
2396
 
 
2397
 
        # Use subarch if supplied; otherwise assume 'generic'.
2398
 
        try:
2399
 
            pxelinux_subarch = request.GET['subarch']
2400
 
        except KeyError:
2401
 
            subarch = 'generic'
2402
 
        else:
2403
 
            # Map from pxelinux namespace subarchitecture names to MAAS
2404
 
            # namespace subarchitecture names. Right now this happens to be a
2405
 
            # 1-1 mapping.
2406
 
            subarch = pxelinux_subarch
 
2438
 
 
2439
        subarch = get_optional_param(request.GET, 'subarch', 'generic')
 
2440
 
 
2441
    # We use as our default label the label of the most recent image for
 
2442
    # the criteria we've assembled above. If there is no latest image
 
2443
    # (which should never happen in reality but may happen in tests), we
 
2444
    # fall back to using 'no-such-image' as our default.
 
2445
    latest_image = BootImage.objects.get_latest_image(
 
2446
        nodegroup, arch, subarch, series, purpose)
 
2447
    if latest_image is None:
 
2448
        # XXX 2014-03-18 gmb bug=1294131:
 
2449
        #     We really ought to raise an exception here so that client
 
2450
        #     and server can handle it according to their needs. At the
 
2451
        #     moment, though, that breaks too many tests in awkward
 
2452
        #     ways.
 
2453
        latest_label = 'no-such-image'
 
2454
    else:
 
2455
        latest_label = latest_image.label
 
2456
    label = get_optional_param(request.GET, 'label', latest_label)
2407
2457
 
2408
2458
    if node is not None:
2409
2459
        # We don't care if the kernel opts is from the global setting or a tag,
2418
2468
    cluster_address = get_mandatory_param(request.GET, "local")
2419
2469
 
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)
2425
2475
 
2426
2476
    return HttpResponse(
2427
2477
        json.dumps(params._asdict()),
2428
2478
        content_type="application/json")
2429
2479
 
2430
2480
 
 
2481
def warn_if_missing_boot_images():
 
2482
    """Show a UI warning if any nodegroups have no boot images.
 
2483
 
 
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.
 
2487
    """
 
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)
 
2504
    else:
 
2505
        discard_persistent_error(COMPONENT.IMPORT_PXE_FILES)
 
2506
 
 
2507
 
 
2508
def summarise_boot_image_object(image_object):
 
2509
    """Return a tuple representing identifying properties of a `BootImage`.
 
2510
 
 
2511
    This function has a counterpart, `summarise_boot_image_dict`.  The two
 
2512
    return the same value for the same boot image.
 
2513
 
 
2514
    :return: A tuple of the image's architecture, subarchitecture, release,
 
2515
        label, and purpose.
 
2516
    """
 
2517
    return (
 
2518
        image_object.architecture,
 
2519
        image_object.subarchitecture,
 
2520
        image_object.release,
 
2521
        image_object.label,
 
2522
        image_object.purpose,
 
2523
        )
 
2524
 
 
2525
 
 
2526
def summarise_boot_image_dict(image_dict):
 
2527
    """Return a tuple representing a reported boot-image dict.
 
2528
 
 
2529
    This is the counterpart to `summarise_boot_image_object`.  The two return
 
2530
    the same value for the same boot image.
 
2531
 
 
2532
    :return: A tuple of the image's architecture, subarchitecture, release,
 
2533
        label, and purpose.
 
2534
    """
 
2535
    return (
 
2536
        image_dict['architecture'],
 
2537
        image_dict.get('subarchitecture', 'generic'),
 
2538
        image_dict['release'],
 
2539
        image_dict.get('label', 'release'),
 
2540
        image_dict['purpose'],
 
2541
        )
 
2542
 
 
2543
 
 
2544
def summarise_reported_images(request):
 
2545
    """Return boot images reported by `request`.
 
2546
 
 
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`.
 
2549
    """
 
2550
    return {
 
2551
        summarise_boot_image_dict(image)
 
2552
        for image in json.loads(get_mandatory_param(request.data, 'images'))
 
2553
        }
 
2554
 
 
2555
 
 
2556
def summarise_stored_images(nodegroup):
 
2557
    """Return boot images for `nodegroup` as found in the database.
 
2558
 
 
2559
    :param nodegroup: The nodegroup whose boot images should be queried.
 
2560
    :return: A set of tuples as produced by `summarise_boot_image_object`.
 
2561
    """
 
2562
    return {
 
2563
        summarise_boot_image_object(image)
 
2564
        for image in BootImage.objects.filter(nodegroup=nodegroup)
 
2565
        }
 
2566
 
 
2567
 
 
2568
def store_boot_images(nodegroup, reported_images, stored_images):
 
2569
    """Store newly reported boot images in the database.
 
2570
 
 
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`.
 
2576
    """
 
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)
 
2582
 
 
2583
 
 
2584
def prune_boot_images(nodegroup, reported_images, stored_images):
 
2585
    """Remove boot images for `nodegroup` which are no longer reported.
 
2586
 
 
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.
 
2590
 
 
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`.
 
2596
    """
 
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)
 
2602
        db_images.delete()
 
2603
 
 
2604
 
 
2605
DISPLAYED_BOOTIMAGE_FIELDS = (
 
2606
    'id',
 
2607
    'release',
 
2608
    'architecture',
 
2609
    'subarchitecture',
 
2610
    'purpose',
 
2611
    'label',
 
2612
)
 
2613
 
 
2614
 
 
2615
class BootImageHandler(OperationsHandler):
 
2616
    """Manage a boot image."""
 
2617
    api_doc_section_name = "Boot image"
 
2618
    create = replace = update = delete = None
 
2619
 
 
2620
    model = BootImage
 
2621
    fields = DISPLAYED_BOOTIMAGE_FIELDS
 
2622
 
 
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)
 
2628
 
 
2629
    @classmethod
 
2630
    def resource_uri(cls, bootimage=None):
 
2631
        if bootimage is None:
 
2632
            id = 'id'
 
2633
            uuid = 'uuid'
 
2634
        else:
 
2635
            id = bootimage.id
 
2636
            uuid = bootimage.nodegroup.uuid
 
2637
        return ('boot_image_handler', (uuid, id))
 
2638
 
 
2639
 
2431
2640
class BootImagesHandler(OperationsHandler):
 
2641
    """Manage the collection of boot images."""
 
2642
    api_doc_section_name = "Boot images"
2432
2643
 
2433
2644
    create = replace = update = delete = None
2434
2645
 
2435
2646
    @classmethod
2436
 
    def resource_uri(cls):
2437
 
        return ('boot_images_handler', [])
 
2647
    def resource_uri(cls, nodegroup=None):
 
2648
        if nodegroup is None:
 
2649
            uuid = 'uuid'
 
2650
        else:
 
2651
            uuid = nodegroup.uuid
 
2652
        return ('boot_images_handler', [uuid])
 
2653
 
 
2654
    def read(self, request, uuid):
 
2655
        """List boot images.
 
2656
 
 
2657
        Get a listing of a cluster's boot images.
 
2658
 
 
2659
        :param uuid: The UUID of the cluster for which the images
 
2660
            should be listed.
 
2661
        """
 
2662
        nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
 
2663
        return BootImage.objects.filter(nodegroup=nodegroup)
2438
2664
 
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.
2442
2668
 
 
2669
        :param uuid: The UUID of the cluster for which the images are
 
2670
            being reported.
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
2446
 
            these images.
 
2673
            `purpose`, and optionally, `label` (which defaults to "release").
 
2674
            These should match the code that determines TFTP paths for these
 
2675
            images.
2447
2676
        """
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'))
2452
 
 
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'])
2460
 
 
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)
2478
 
        else:
2479
 
            discard_persistent_error(COMPONENT.IMPORT_PXE_FILES)
2480
 
 
 
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")
2482
2685
 
2483
2686
 
2492
2695
 
2493
2696
    This functionality is only available to administrators.
2494
2697
    """
 
2698
    api_doc_section_name = "Commissioning script"
2495
2699
 
2496
2700
    update = delete = None
2497
2701
 
2547
2751
 
2548
2752
    This functionality is only available to administrators.
2549
2753
    """
 
2754
    api_doc_section_name = "Commissioning script"
2550
2755
 
2551
2756
    model = CommissioningScript
2552
2757
    fields = ('name', 'content')
2583
2788
 
2584
2789
class CommissioningResultsHandler(OperationsHandler):
2585
2790
    """Read the collection of NodeCommissionResult in the MAAS."""
 
2791
    api_doc_section_name = "Commissioning results"
2586
2792
    create = read = update = delete = None
2587
2793
 
2588
2794
    model = NodeCommissionResult
2668
2874
    This functionality is only available to administrators.  Other users can
2669
2875
    view physical zones, but not modify them.
2670
2876
    """
 
2877
    api_doc_section_name = "Zone"
2671
2878
    model = Zone
2672
2879
    fields = ('name', 'description')
2673
2880
 
2706
2913
 
2707
2914
 
2708
2915
class ZonesHandler(OperationsHandler):
2709
 
    """API for physical zones."""
 
2916
    """Manage physical zones."""
 
2917
    api_doc_section_name = "Zones"
2710
2918
    update = delete = None
2711
2919
 
2712
2920
    @classmethod
2737
2945
 
2738
2946
 
2739
2947
class NetworkHandler(OperationsHandler):
 
2948
    """Manage a network."""
 
2949
    api_doc_section_name = "Network"
 
2950
 
2740
2951
    model = Network
2741
2952
    fields = ('name', 'ip', 'netmask', 'vlan_tag', 'description')
2742
2953
 
2782
2993
 
2783
2994
    @admin_method
2784
2995
    @operation(idempotent=False)
2785
 
    def connect_nodes(self, request, name):
2786
 
        """Connect the given nodes to this network.
2787
 
 
2788
 
        Connecting a node to a network which it is already connected to does
2789
 
        nothing.
2790
 
 
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.
 
2998
 
 
2999
        These MAC addresses must belong to nodes in the MAAS, and have been
 
3000
        registered as such in MAAS.
 
3001
 
 
3002
        Connecting a network interface to a network which it is already
 
3003
        connected to does nothing.
 
3004
 
 
3005
        :param macs: A list of node MAC addresses, in text form.
2792
3006
        """
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())
 
3011
        form.save()
2798
3012
 
2799
3013
    @admin_method
2800
3014
    @operation(idempotent=False)
2801
 
    def disconnect_nodes(self, request, name):
2802
 
        """Disconnect the given nodes from this network.
2803
 
 
2804
 
        Removing a node from a network which it is not connected to does
2805
 
        nothing.
2806
 
 
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.
 
3017
 
 
3018
        Removing a MAC address from a network which it is not connected to
 
3019
        does nothing.
 
3020
 
 
3021
        :param macs: A list of node MAC addresses, in text form.
2808
3022
        """
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())
 
3027
        form.save()
2814
3028
 
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.
2818
3032
 
2819
 
        Only the nodes visible to the requesting user are returned.
 
3033
        Only MAC addresses for nodes visible to the requesting user are
 
3034
        returned.
2820
3035
        """
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')
2825
3042
 
2826
3043
    @classmethod
2827
3044
    def resource_uri(cls, network=None):
2834
3051
 
2835
3052
 
2836
3053
class NetworksHandler(OperationsHandler):
2837
 
    """API for networks."""
 
3054
    """Manage the networks."""
 
3055
    api_doc_section_name = "Networks"
2838
3056
 
2839
3057
    update = delete = None
2840
3058