def test__compose_sends_default_storage_pool_id(self): request = MagicMock() pod = make_pod_with_hints() # Mock the RPC client. client = MagicMock() mock_getClient = self.patch(pods_module, "getClientFromIdentifiers") mock_getClient.return_value = succeed(client) # Mock the result of the composed machine. composed_machine, pod_hints = self.make_compose_machine_result(pod) mock_compose_machine = self.patch(pods_module, "compose_machine") mock_compose_machine.return_value = succeed( (composed_machine, pod_hints)) # Mock start_commissioning so it doesn't use post commit hooks. self.patch(Machine, "start_commissioning") form = ComposeMachineForm(data={}, request=request, pod=pod) self.assertTrue(form.is_valid()) form.compose() self.assertThat(mock_compose_machine, MockCalledOnceWith( ANY, pod.power_type, { 'default_storage_pool_id': pod.default_storage_pool.pool_id, }, form.get_requested_machine(), pod_id=pod.id, name=pod.name))
def test__compose_sets_domain_and_zone(self): request = MagicMock() pod = make_pod_with_hints() # Mock the RPC client. client = MagicMock() mock_getClient = self.patch(pods_module, "getClientFromIdentifiers") mock_getClient.return_value = succeed(client) # Mock the result of the composed machine. composed_machine, pod_hints = self.make_compose_machine_result(pod) mock_compose_machine = self.patch(pods_module, "compose_machine") mock_compose_machine.return_value = succeed( (composed_machine, pod_hints)) domain = factory.make_Domain() zone = factory.make_Zone() form = ComposeMachineForm(data={ "domain": domain.id, "zone": zone.id, "skip_commissioning": 'true', }, request=request, pod=pod) self.assertTrue(form.is_valid()) created_machine = form.compose() self.assertThat(created_machine, MatchesAll( IsInstance(Machine), MatchesStructure( domain=Equals(domain), zone=Equals(zone))))
def test__compose_with_skip_commissioning_passed(self): request = MagicMock() pod = make_pod_with_hints() # Mock the RPC client. client = MagicMock() mock_getClient = self.patch(pods_module, "getClientFromIdentifiers") mock_getClient.return_value = succeed(client) # Mock the result of the composed machine. composed_machine, pod_hints = self.make_compose_machine_result(pod) mock_compose_machine = self.patch(pods_module, "compose_machine") mock_compose_machine.return_value = succeed( (composed_machine, pod_hints)) # Mock start_commissioning so it doesn't use post commit hooks. mock_commissioning = self.patch(Machine, "start_commissioning") form = ComposeMachineForm(data={}, request=request, pod=pod) self.assertTrue(form.is_valid()) created_machine = form.compose(skip_commissioning=True) self.assertThat(created_machine, MatchesAll( IsInstance(Machine), MatchesStructure( cpu_count=Equals(1), memory=Equals(1024), cpu_speed=Equals(300)))) self.assertThat(mock_commissioning, MockNotCalled())
def compose(self, request, id): """Compose a machine from Pod. All fields below are optional: :param cores: Minimum number of CPU cores. :type cores: unicode :param memory: Minimum amount of memory (MiB). :type memory: unicode :param cpu_speed: Minimum amount of CPU speed (MHz). :type cpu_speed: unicode :param architecture: Architecture for the machine. Must be an architecture that the pod supports. :param architecture: unicode :param storage: A list of storage constraint identifiers, in the form: <label>:<size>(<tag>[,<tag>[,...])][,<label>:...] :type storage: unicode :param hostname: Hostname for the newly composed machine. :type hostname: unicode :param domain: ID of domain to place the newly composed machine in. :type domain: unicode :param zone: ID of zone place the newly composed machine in. :type zone: unicode :param pool: ID of resource pool to place the newly composed machine in. :type pool: unicode Returns 404 if the pod is not found. Returns 403 if the user does not have permission to compose machine. """ pod = get_object_or_404(Pod, id=id) if Capabilities.COMPOSABLE not in pod.capabilities: raise MAASAPIValidationError("Pod does not support composability.") form = ComposeMachineForm(data=request.data, pod=pod, request=request) if form.is_valid(): machine = form.compose() return { 'system_id': machine.system_id, 'resource_uri': reverse('machine_handler', kwargs={'system_id': machine.system_id}) } else: raise MAASAPIValidationError(form.errors)
def test__compose_uses_pod_pool(self): request = MagicMock() pod = make_pod_with_hints() pod.pool = factory.make_ResourcePool() pod.save() # Mock the RPC client. client = MagicMock() mock_getClient = self.patch(pods_module, "getClientFromIdentifiers") mock_getClient.return_value = succeed(client) # Mock the result of the composed machine. composed_machine, pod_hints = self.make_compose_machine_result(pod) mock_compose_machine = self.patch(pods_module, "compose_machine") mock_compose_machine.return_value = succeed( (composed_machine, pod_hints)) form = ComposeMachineForm(data={ "skip_commissioning": 'true', }, request=request, pod=pod) self.assertTrue(form.is_valid()) created_machine = form.compose() self.assertEqual(pod.pool, created_machine.pool)
def compose(self, request, id): """@description-title Compose a pod machine @description Compose a new machine from a pod. @param (int) "cores" [required=false] The minimum number of CPU cores. @param (int) "memory" [required=false] The minimum amount of memory, specified in MiB (e.g. 2 MiB == 2*1024*1024). @param (int) "cores" [required=false] The minimum number of CPU cores. @param (int) "cpu_speed" [required=false] The minimum CPU speed, specified in MHz. @param (string) "architecture" [required=false] The architecture of the new machine (e.g. amd64). This must be an architecture the pod supports. @param (string) "storage" [required=false] A list of storage constraint identifiers in the form ``label:size(tag,tag,...), label:size(tag,tag,...)``. For more information please see the CLI pod management page of the official MAAS documentation. @param (string) "interfaces" [required=false,formatting=true] A labeled constraint map associating constraint labels with desired interface properties. MAAS will assign interfaces that match the given interface properties. Format: ``label:key=value,key=value,...`` Keys: - ``id``: Matches an interface with the specific id - ``fabric``: Matches an interface attached to the specified fabric. - ``fabric_class``: Matches an interface attached to a fabric with the specified class. - ``ip``: Matches an interface whose VLAN is on the subnet implied by the given IP address, and allocates the specified IP address for the machine on that interface (if it is available). - ``mode``: Matches an interface with the specified mode. (Currently, the only supported mode is "unconfigured".) - ``name``: Matches an interface with the specified name. (For example, "eth0".) - ``hostname``: Matches an interface attached to the node with the specified hostname. - ``subnet``: Matches an interface attached to the specified subnet. - ``space``: Matches an interface attached to the specified space. - ``subnet_cidr``: Matches an interface attached to the specified subnet CIDR. (For example, "192.168.0.0/24".) - ``type``: Matches an interface of the specified type. (Valid types: "physical", "vlan", "bond", "bridge", or "unknown".) - ``vlan``: Matches an interface on the specified VLAN. - ``vid``: Matches an interface on a VLAN with the specified VID. - ``tag``: Matches an interface tagged with the specified tag. @param (string) "hostname" [required=false] The hostname of the newly composed machine. @param (int) "domain" [required=false] The ID of the domain in which to put the newly composed machine. @param (int) "zone" [required=false] The ID of the zone in which to put the newly composed machine. @param (int) "pool" [required=false] The ID of the pool in which to put the newly composed machine. @success (http-status-code) "200" 200 @success (json) "success-json" A JSON object containing the new machine ID and resource URI. @success-example (json) "success-json" [exkey=compose] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" No pod with that ID can be found. @error-example "not-found" Not Found @error (http-status-code) "403" 403 @error (content) "no-perms" The user does not have the permissions to delete the pod. @error-example (content) "no-perms" This method is reserved for admin users. """ pod = Pod.objects.get_pod_or_404(id, request.user, PodPermission.compose) if Capabilities.COMPOSABLE not in pod.capabilities: raise MAASAPIValidationError("Pod does not support composability.") form = ComposeMachineForm(data=request.data, pod=pod, request=request) if form.is_valid(): machine = form.compose() return { "system_id": machine.system_id, "resource_uri": reverse("machine_handler", kwargs={"system_id": machine.system_id}), } else: raise MAASAPIValidationError(form.errors)
def compose(self, request, id): """Compose a machine from Pod. All fields below are optional: :param cores: Minimum number of CPU cores. :type cores: unicode :param memory: Minimum amount of memory (MiB). :type memory: unicode :param cpu_speed: Minimum amount of CPU speed (MHz). :type cpu_speed: unicode :param architecture: Architecture for the machine. Must be an architecture that the pod supports. :param architecture: unicode :param storage: A list of storage constraint identifiers, in the form: <label>:<size>(<tag>[,<tag>[,...])][,<label>:...] :type storage: unicode :param interfaces: A labeled constraint map associating constraint labels with interface properties that should be matched. Returned nodes must have one or more interface matching the specified constraints. The labeled constraint map must be in the format: ``<label>:<key>=<value>[,<key2>=<value2>[,...]]`` Each key can be one of the following: - id: Matches an interface with the specific id - fabric: Matches an interface attached to the specified fabric. - fabric_class: Matches an interface attached to a fabric with the specified class. - ip: Matches an interface whose VLAN is on the subnet implied by the given IP address, and allocates the specified IP address for the machine on that interface (if it is available). - mode: Matches an interface with the specified mode. (Currently, the only supported mode is "unconfigured".) - name: Matches an interface with the specified name. (For example, "eth0".) - hostname: Matches an interface attached to the node with the specified hostname. - subnet: Matches an interface attached to the specified subnet. - space: Matches an interface attached to the specified space. - subnet_cidr: Matches an interface attached to the specified subnet CIDR. (For example, "192.168.0.0/24".) - type: Matches an interface of the specified type. (Valid types: "physical", "vlan", "bond", "bridge", or "unknown".) - vlan: Matches an interface on the specified VLAN. - vid: Matches an interface on a VLAN with the specified VID. - tag: Matches an interface tagged with the specified tag. :type interfaces: unicode :param hostname: Hostname for the newly composed machine. :type hostname: unicode :param domain: ID of domain to place the newly composed machine in. :type domain: unicode :param zone: ID of zone place the newly composed machine in. :type zone: unicode :param pool: ID of resource pool to place the newly composed machine in. :type pool: unicode Returns 404 if the pod is not found. Returns 403 if the user does not have permission to compose machine. """ pod = get_object_or_404(Pod, id=id) if Capabilities.COMPOSABLE not in pod.capabilities: raise MAASAPIValidationError("Pod does not support composability.") form = ComposeMachineForm(data=request.data, pod=pod, request=request) if form.is_valid(): machine = form.compose() return { 'system_id': machine.system_id, 'resource_uri': reverse('machine_handler', kwargs={'system_id': machine.system_id}) } else: raise MAASAPIValidationError(form.errors)