Anton Troyanov has proposed merging 
~troyanov/maas:migrate-node-power-credentials into maas:master.

Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~troyanov/maas/+git/maas/+merge/431804
-- 
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/src/maasserver/api/nodes.py b/src/maasserver/api/nodes.py
index 04c2457..891b335 100644
--- a/src/maasserver/api/nodes.py
+++ b/src/maasserver/api/nodes.py
@@ -583,7 +583,7 @@ class NodeHandler(OperationsHandler):
         node = Node.objects.get_node_or_404(
             system_id, request.user, NodePermission.admin_read
         )
-        return node.power_parameters
+        return node.get_power_parameters()
 
 
 class AnonNodeHandler(AnonymousOperationsHandler):
@@ -1223,5 +1223,6 @@ class PowersMixin:
             machines = self.base_model.objects.filter(system_id__in=match_ids)
 
         return {
-            machine.system_id: machine.power_parameters for machine in machines
+            machine.system_id: machine.get_power_parameters()
+            for machine in machines
         }
diff --git a/src/maasserver/api/pods.py b/src/maasserver/api/pods.py
index 58c8910..dc3d2d2 100644
--- a/src/maasserver/api/pods.py
+++ b/src/maasserver/api/pods.py
@@ -253,7 +253,7 @@ class VmHostHandler(OperationsHandler):
             This method is reserved for admin users.
         """
         pod = Pod.objects.get_pod_or_404(id, request.user, PodPermission.edit)
-        return pod.power_parameters
+        return pod.get_power_parameters()
 
     @admin_method
     @operation(idempotent=False)
diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
index 1ac2b30..fe5f429 100644
--- a/src/maasserver/api/tests/test_enlistment.py
+++ b/src/maasserver/api/tests/test_enlistment.py
@@ -104,7 +104,7 @@ class TestEnlistmentAPI(APITestCase.ForAnonymousAndUserAndAdmin):
         self.assertEqual(http.client.OK, response.status_code)
         [machine] = Machine.objects.filter(hostname=hostname)
         for key, value in power_parameters.items():
-            self.assertEqual(machine.bmc.power_parameters[key], value)
+            self.assertEqual(machine.bmc.get_power_parameters()[key], value)
         self.assertEqual(power_type, machine.power_type)
 
     def test_POST_create_creates_machine_with_arch_only(self):
@@ -463,7 +463,7 @@ class TestAnonymousEnlistmentAPI(APITestCase.ForAnonymous):
         self.assertEqual(hostname, machine.hostname)
         self.assertEqual(architecture, machine.architecture)
         for key, value in power_parameters.items():
-            self.assertEqual(machine.bmc.power_parameters[key], value)
+            self.assertEqual(machine.bmc.get_power_parameters()[key], value)
         self.assertThat(mock_create_machine, MockNotCalled())
         self.assertEqual(
             machine.system_id, json_load_bytes(response.content)["system_id"]
@@ -664,7 +664,7 @@ class TestSimpleUserLoggedInEnlistmentAPI(APITestCase.ForUser):
                 "power_id": new_power_id,
                 "power_address": new_power_address,
             },
-            machine.power_parameters,
+            machine.get_power_parameters(),
         )
 
     def test_POST_returns_limited_fields(self):
@@ -790,7 +790,7 @@ class TestAdminLoggedInEnlistmentAPI(APITestCase.ForAdmin):
             system_id=json_load_bytes(response.content)["system_id"]
         )
         self.assertEqual("manual", machine.power_type)
-        self.assertEqual({}, machine.power_parameters)
+        self.assertEqual({}, machine.get_power_parameters())
 
     def test_POST_sets_power_parameters_field(self):
         # The api allows the setting of a Machine's power_parameters field.
@@ -822,7 +822,7 @@ class TestAdminLoggedInEnlistmentAPI(APITestCase.ForAdmin):
                 "power_pass": new_power_pass,
                 "power_address": new_power_address,
             },
-            reload_object(machine).power_parameters,
+            reload_object(machine).get_power_parameters(),
         )
 
     def test_POST_updates_power_parameters_rejects_unknown_param(self):
@@ -869,7 +869,7 @@ class TestAdminLoggedInEnlistmentAPI(APITestCase.ForAdmin):
             system_id=json_load_bytes(response.content)["system_id"]
         )
         self.assertEqual(
-            {"param": param}, reload_object(machine).power_parameters
+            {"param": param}, reload_object(machine).get_power_parameters()
         )
 
     def test_POST_admin_creates_machine_in_commissioning_state(self):
diff --git a/src/maasserver/api/tests/test_machine.py b/src/maasserver/api/tests/test_machine.py
index c67d4a1..a2bd82c 100644
--- a/src/maasserver/api/tests/test_machine.py
+++ b/src/maasserver/api/tests/test_machine.py
@@ -2237,7 +2237,7 @@ class TestMachineAPI(APITestCase.ForUser):
                 "power_pass": new_power_pass,
                 "power_address": new_power_address,
             },
-            reload_object(machine).power_parameters,
+            reload_object(machine).get_power_parameters(),
         )
 
     def test_PUT_updates_cpu_memory(self):
@@ -2277,7 +2277,7 @@ class TestMachineAPI(APITestCase.ForUser):
             (response.status_code, json_load_bytes(response.content)),
         )
         self.assertEqual(
-            power_parameters, reload_object(machine).power_parameters
+            power_parameters, reload_object(machine).get_power_parameters()
         )
 
     def test_PUT_updates_power_type_default_resets_params(self):
@@ -2297,7 +2297,11 @@ class TestMachineAPI(APITestCase.ForUser):
 
         machine = reload_object(machine)
         self.assertEqual(
-            (http.client.OK, machine.power_type, machine.power_parameters),
+            (
+                http.client.OK,
+                machine.power_type,
+                machine.get_power_parameters(),
+            ),
             (response.status_code, "", {}),
         )
 
@@ -2326,7 +2330,7 @@ class TestMachineAPI(APITestCase.ForUser):
             (response.status_code, json_load_bytes(response.content)),
         )
         self.assertEqual(
-            power_parameters, reload_object(machine).power_parameters
+            power_parameters, reload_object(machine).get_power_parameters()
         )
 
     def test_PUT_updates_power_type_empty_skip_check_to_force_params(self):
@@ -2353,7 +2357,11 @@ class TestMachineAPI(APITestCase.ForUser):
 
         machine = reload_object(machine)
         self.assertEqual(
-            (http.client.OK, machine.power_type, machine.power_parameters),
+            (
+                http.client.OK,
+                machine.power_type,
+                machine.get_power_parameters(),
+            ),
             (response.status_code, "", {"param": new_param}),
         )
 
@@ -2378,7 +2386,8 @@ class TestMachineAPI(APITestCase.ForUser):
 
         self.assertEqual(http.client.OK, response.status_code)
         self.assertEqual(
-            {new_param: new_value}, reload_object(machine).power_parameters
+            {new_param: new_value},
+            reload_object(machine).get_power_parameters(),
         )
 
     def test_PUT_updates_power_parameters_empty_string(self):
@@ -2400,7 +2409,7 @@ class TestMachineAPI(APITestCase.ForUser):
 
         self.assertEqual(http.client.OK, response.status_code)
         self.assertEqual(
-            "", reload_object(machine).power_parameters["power_pass"]
+            "", reload_object(machine).get_power_parameters()["power_pass"]
         )
 
     def test_PUT_sets_zone(self):
diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py
index 230ecf8..821a2ce 100644
--- a/src/maasserver/api/tests/test_machines.py
+++ b/src/maasserver/api/tests/test_machines.py
@@ -204,7 +204,8 @@ class TestMachinesAPI(APITestCase.ForUser):
         machine = Machine.objects.get(system_id=system_id)
         self.assertEqual(machine.hostname, hostname)
         self.assertCountEqual(
-            workaround_flags, machine.power_parameters["workaround_flags"]
+            workaround_flags,
+            machine.get_power_parameters()["workaround_flags"],
         )
 
     def test_POST_creates_ipmi_machine_sets_mac_addresses_empty_no_arch(self):
@@ -237,7 +238,7 @@ class TestMachinesAPI(APITestCase.ForUser):
             Equals(set()),
         )
         self.assertEqual(
-            power_address, machine.power_parameters["power_address"]
+            power_address, machine.get_power_parameters()["power_address"]
         )
 
     def test_POST_when_logged_in_creates_machine_in_declared_state(self):
@@ -323,7 +324,7 @@ class TestMachinesAPI(APITestCase.ForUser):
         )["system_id"]
         machine = Machine.objects.get(system_id=system_id)
         self.assertEqual("", machine.power_type)
-        self.assertEqual({}, machine.power_parameters)
+        self.assertEqual({}, machine.get_power_parameters())
 
     def test_POST_handles_error_when_unable_to_access_bmc(self):
         # Regression test for LP1600328
@@ -346,9 +347,9 @@ class TestMachinesAPI(APITestCase.ForUser):
         machine = Machine.objects.get(system_id=parsed_result["system_id"])
         self.assertEqual("virsh", parsed_result["power_type"])
         self.assertEqual(
-            power_address, machine.power_parameters["power_address"]
+            power_address, machine.get_power_parameters()["power_address"]
         )
-        self.assertEqual(power_id, machine.power_parameters["power_id"])
+        self.assertEqual(power_id, machine.get_power_parameters()["power_id"])
 
     def test_POST_sets_description(self):
         # Regression test for LP1707562
diff --git a/src/maasserver/api/tests/test_node.py b/src/maasserver/api/tests/test_node.py
index c37b11a..63ea132 100644
--- a/src/maasserver/api/tests/test_node.py
+++ b/src/maasserver/api/tests/test_node.py
@@ -515,7 +515,7 @@ class TestPowerParameters(APITestCase.ForUser):
             http.client.OK, response.status_code, response.content
         )
         parsed_params = json_load_bytes(response.content)
-        self.assertEqual(node.power_parameters, parsed_params)
+        self.assertEqual(node.get_power_parameters(), parsed_params)
 
     def test_get_power_parameters_user(self):
         power_parameters = {factory.make_string(): factory.make_string()}
@@ -542,7 +542,7 @@ class TestPowerParameters(APITestCase.ForUser):
             http.client.OK, response.status_code, response.content
         )
         parsed_params = json_load_bytes(response.content)
-        self.assertEqual(node.power_parameters, parsed_params)
+        self.assertEqual(node.get_power_parameters(), parsed_params)
 
     def test_get_power_parameters_rbac_pool_user(self):
         self.patch(auth, "validate_user_external_auth").return_value = True
@@ -570,7 +570,7 @@ class TestPowerParameters(APITestCase.ForUser):
             http.client.OK, response.status_code, response.content
         )
         parsed_params = json_load_bytes(response.content)
-        self.assertEqual(node.power_parameters, parsed_params)
+        self.assertEqual(node.get_power_parameters(), parsed_params)
 
     def test_get_power_parameters_view_lock(self):
         self.become_admin()
@@ -585,7 +585,7 @@ class TestPowerParameters(APITestCase.ForUser):
             http.client.OK, response.status_code, response.content
         )
         parsed_params = json_load_bytes(response.content)
-        self.assertEqual(node.power_parameters, parsed_params)
+        self.assertEqual(node.get_power_parameters(), parsed_params)
 
 
 class TestSetWorkloadAnnotations(APITestCase.ForUser):
diff --git a/src/maasserver/api/tests/test_nodes.py b/src/maasserver/api/tests/test_nodes.py
index 88fa7e6..91922f8 100644
--- a/src/maasserver/api/tests/test_nodes.py
+++ b/src/maasserver/api/tests/test_nodes.py
@@ -792,7 +792,8 @@ class TestPowersMixin(APITestCase.ForUser):
         )
         parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET))
         expected = {
-            machine.system_id: machine.power_parameters for machine in machines
+            machine.system_id: machine.get_power_parameters()
+            for machine in machines
         }
         self.assertEqual(expected, parsed)
 
@@ -817,7 +818,7 @@ class TestPowersMixin(APITestCase.ForUser):
         )
         parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET))
         expected = {
-            machine.system_id: machine.power_parameters
+            machine.system_id: machine.get_power_parameters()
             for machine in expected_machines
         }
         self.assertEqual(expected, parsed)
diff --git a/src/maasserver/api/tests/test_pods.py b/src/maasserver/api/tests/test_pods.py
index 4eff05b..24a4f7e 100644
--- a/src/maasserver/api/tests/test_pods.py
+++ b/src/maasserver/api/tests/test_pods.py
@@ -214,7 +214,7 @@ class TestPodsAPIAdmin(PodAPITestForAdmin, PodMixin):
         self.assertEqual(http.client.OK, response.status_code)
         parsed_result = json_load_bytes(response.content)
         pod = Pod.objects.get(id=parsed_result["id"])
-        self.assertEqual(pod.power_parameters["project"], "default")
+        self.assertEqual(pod.get_power_parameters()["project"], "default")
 
     def test_create_creates_pod_with_default_resource_pool(self):
         self.patch(pods, "post_commit_do")
@@ -406,7 +406,7 @@ class TestPodAPIAdmin(PodAPITestForAdmin, PodMixin):
         self.assertEqual(new_name, pod.name)
         self.assertEqual(new_pool, pod.pool)
         self.assertCountEqual(new_tags, pod.tags)
-        self.assertEqual(new_power_parameters, pod.power_parameters)
+        self.assertEqual(new_power_parameters, pod.get_power_parameters())
         self.assertEqual(new_zone, pod.zone)
 
     def test_PUT_updates_discovers_syncs_and_returns_pod(self):
@@ -451,7 +451,7 @@ class TestPodAPIAdmin(PodAPITestForAdmin, PodMixin):
         pod.refresh_from_db()
         self.assertIsNotNone(Tag.objects.get(name="pod-console-logging"))
         self.assertEqual(new_name, pod.name)
-        self.assertEqual(power_parameters, pod.power_parameters)
+        self.assertEqual(power_parameters, pod.get_power_parameters())
 
     def test_PUT_update_updates_pod_default_macvlan_mode(self):
         self.patch(pods, "post_commit_do")
@@ -481,16 +481,16 @@ class TestPodAPIAdmin(PodAPITestForAdmin, PodMixin):
 
     def test_parameters_returns_pod_parameters(self):
         pod = factory.make_Pod()
-        pod.power_parameters = {
-            factory.make_name("key"): factory.make_name("value")
-        }
+        pod.set_power_parameters(
+            {factory.make_name("key"): factory.make_name("value")}
+        )
         pod.save()
         response = self.client.get(get_pod_uri(pod), {"op": "parameters"})
         self.assertEqual(
             http.client.OK, response.status_code, response.content
         )
         parsed_params = json_load_bytes(response.content)
-        self.assertEqual(pod.power_parameters, parsed_params)
+        self.assertEqual(pod.get_power_parameters(), parsed_params)
 
     def test_compose_not_allowed_on_none_composable_pod(self):
         pod = make_pod_with_hints()
diff --git a/src/maasserver/clusterrpc/tests/test_driver_parameters.py b/src/maasserver/clusterrpc/tests/test_driver_parameters.py
index 100bb5a..a50c281 100644
--- a/src/maasserver/clusterrpc/tests/test_driver_parameters.py
+++ b/src/maasserver/clusterrpc/tests/test_driver_parameters.py
@@ -277,6 +277,7 @@ class TestMakeSettingField(MAASServerTestCase):
             "choices": [],
             "default": "",
             "scope": "bmc",
+            "secret": False,
         }
         self.assertEqual(expected_field, json_field)
 
@@ -289,6 +290,7 @@ class TestMakeSettingField(MAASServerTestCase):
             "choices": [["spam", "Spam"], ["eggs", "Eggs"]],
             "default": "spam",
             "scope": "bmc",
+            "secret": False,
         }
         json_field = make_setting_field(**expected_field)
         self.assertEqual(expected_field, json_field)
@@ -304,7 +306,7 @@ class TestMakeSettingField(MAASServerTestCase):
 
     def test_creates_password_fields(self):
         json_field = make_setting_field(
-            "some_field", "Some Label", field_type="password"
+            "some_field", "Some Label", field_type="password", secret=True
         )
         expected_field = {
             "name": "some_field",
@@ -314,6 +316,7 @@ class TestMakeSettingField(MAASServerTestCase):
             "choices": [],
             "default": "",
             "scope": "bmc",
+            "secret": True,
         }
         self.assertEqual(expected_field, json_field)
 
diff --git a/src/maasserver/clusterrpc/tests/test_pods.py b/src/maasserver/clusterrpc/tests/test_pods.py
index 3c42e2b..36badc6 100644
--- a/src/maasserver/clusterrpc/tests/test_pods.py
+++ b/src/maasserver/clusterrpc/tests/test_pods.py
@@ -356,7 +356,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
             pod.name,
             pod.power_type,
             node.system_id,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             token.consumer.key,
             token.key,
             token.secret,
@@ -371,7 +371,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
                 name=pod.name,
                 type=pod.power_type,
                 system_id=node.system_id,
-                context=pod.power_parameters,
+                context=pod.get_power_parameters(),
                 consumer_key=token.consumer.key,
                 token_key=token.key,
                 token_secret=token.secret,
@@ -395,7 +395,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
             pod.name,
             pod.power_type,
             node.system_id,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             token.consumer.key,
             token.key,
             token.secret,
@@ -423,7 +423,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
             pod.name,
             pod.power_type,
             node.system_id,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             token.consumer.key,
             token.key,
             token.secret,
@@ -453,7 +453,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
             pod.name,
             pod.power_type,
             node.system_id,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             token.consumer.key,
             token.key,
             token.secret,
@@ -483,7 +483,7 @@ class TestSendPodCommissioningResults(MAASServerTestCase):
             pod.name,
             pod.power_type,
             node.system_id,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             token.consumer.key,
             token.key,
             token.secret,
@@ -505,7 +505,7 @@ class TestComposeMachine(MAASServerTestCase):
         machine, hints = wait_for_reactor(compose_machine)(
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             sentinel.request,
             pod.id,
             pod.name,
@@ -516,7 +516,7 @@ class TestComposeMachine(MAASServerTestCase):
             MockCalledOnceWith(
                 ComposeMachine,
                 type=pod.power_type,
-                context=pod.power_parameters,
+                context=pod.get_power_parameters(),
                 request=sentinel.request,
                 pod_id=pod.id,
                 name=pod.name,
@@ -535,7 +535,7 @@ class TestComposeMachine(MAASServerTestCase):
             wait_for_reactor(compose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             sentinel.request,
             pod.id,
             pod.name,
@@ -556,7 +556,7 @@ class TestComposeMachine(MAASServerTestCase):
             wait_for_reactor(compose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             sentinel.request,
             pod.id,
             pod.name,
@@ -578,7 +578,7 @@ class TestComposeMachine(MAASServerTestCase):
             wait_for_reactor(compose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             sentinel.request,
             pod.id,
             pod.name,
@@ -599,7 +599,7 @@ class TestComposeMachine(MAASServerTestCase):
             wait_for_reactor(compose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             sentinel.request,
             pod.id,
             pod.name,
@@ -622,7 +622,11 @@ class TestDecomposeMachine(MAASServerTestCase):
         client.return_value = succeed({"hints": hints})
 
         result = wait_for_reactor(decompose_machine)(
-            client, pod.power_type, pod.power_parameters, pod.id, pod.name
+            client,
+            pod.power_type,
+            pod.get_power_parameters(),
+            pod.id,
+            pod.name,
         )
 
         self.assertThat(
@@ -630,7 +634,7 @@ class TestDecomposeMachine(MAASServerTestCase):
             MockCalledOnceWith(
                 DecomposeMachine,
                 type=pod.power_type,
-                context=pod.power_parameters,
+                context=pod.get_power_parameters(),
                 pod_id=pod.id,
                 name=pod.name,
             ),
@@ -647,7 +651,7 @@ class TestDecomposeMachine(MAASServerTestCase):
             wait_for_reactor(decompose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             pod.id,
             pod.name,
         )
@@ -667,7 +671,7 @@ class TestDecomposeMachine(MAASServerTestCase):
             wait_for_reactor(decompose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             pod.id,
             pod.name,
         )
@@ -688,7 +692,7 @@ class TestDecomposeMachine(MAASServerTestCase):
             wait_for_reactor(decompose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             pod.id,
             pod.name,
         )
@@ -708,7 +712,7 @@ class TestDecomposeMachine(MAASServerTestCase):
             wait_for_reactor(decompose_machine),
             client,
             pod.power_type,
-            pod.power_parameters,
+            pod.get_power_parameters(),
             pod.id,
             pod.name,
         )
diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
index 93e90af..1a01fea 100644
--- a/src/maasserver/forms/__init__.py
+++ b/src/maasserver/forms/__init__.py
@@ -286,7 +286,7 @@ class WithPowerTypeMixin:
 
         # Integrate the machines existing parameters if unset by form.
         if machine:
-            for key, value in machine.power_parameters.items():
+            for key, value in machine.get_power_parameters().items():
                 if parameters.get(key) is None:
                     parameters[key] = value
         return parameters
@@ -319,7 +319,7 @@ class WithPowerTypeMixin:
         if form.instance is not None:
             if form.instance.power_type != "":
                 form.initial[type_field_name] = form.instance.power_type
-            if form.instance.power_parameters != "":
+            if form.instance.get_power_parameters() != "":
                 for key, value in parameters.items():
                     form.initial[f"{params_field_name}_{key}"] = value
 
diff --git a/src/maasserver/forms/tests/test_controller.py b/src/maasserver/forms/tests/test_controller.py
index 61385f7..ca5ffa5 100644
--- a/src/maasserver/forms/tests/test_controller.py
+++ b/src/maasserver/forms/tests/test_controller.py
@@ -63,7 +63,7 @@ class TestControllerForm(MAASServerTestCase):
         )
         rack = form.save()
         self.assertEqual(
-            power_parameters_field, rack.power_parameters["field"]
+            power_parameters_field, rack.get_power_parameters()["field"]
         )
 
     def test_sets_zone(self):
diff --git a/src/maasserver/forms/tests/test_machine.py b/src/maasserver/forms/tests/test_machine.py
index de20bef..2916c22 100644
--- a/src/maasserver/forms/tests/test_machine.py
+++ b/src/maasserver/forms/tests/test_machine.py
@@ -519,7 +519,7 @@ class TestAdminMachineForm(MAASServerTestCase):
 
         self.assertEqual(
             (hostname, power_type, {"field": power_parameters_field}),
-            (node.hostname, node.power_type, node.power_parameters),
+            (node.hostname, node.power_type, node.get_power_parameters()),
         )
 
     def test_AdminMachineForm_doesnt_changes_power_parameters(self):
@@ -536,7 +536,7 @@ class TestAdminMachineForm(MAASServerTestCase):
             instance=node,
         )
         node = form.save()
-        self.assertEqual(power_parameters, node.power_parameters)
+        self.assertEqual(power_parameters, node.get_power_parameters())
 
     def test_AdminMachineForm_doesnt_change_power_type(self):
         power_type = factory.pick_power_type()
@@ -640,7 +640,7 @@ class TestAdminMachineWithMACAddressForm(MAASServerTestCase):
         )
         self.assertTrue(form.is_valid())
         machine = form.save()
-        power_params = machine.bmc.power_parameters
+        power_params = machine.bmc.get_power_parameters()
         self.assertIn("certificate", power_params)
         self.assertIn("key", power_params)
         cert = Certificate.from_pem(
@@ -648,5 +648,7 @@ class TestAdminMachineWithMACAddressForm(MAASServerTestCase):
         )
         self.assertEqual(cert.cn(), sample_cert.cn())
         # cert/key are not per-instance parameters
-        self.assertNotIn("certificate", machine.instance_power_parameters)
-        self.assertNotIn("key", machine.instance_power_parameters)
+        self.assertNotIn(
+            "certificate", machine.get_instance_power_parameters()
+        )
+        self.assertNotIn("key", machine.get_instance_power_parameters())
diff --git a/src/maasserver/forms/tests/test_pods.py b/src/maasserver/forms/tests/test_pods.py
index 9a4dd86..beac59b 100644
--- a/src/maasserver/forms/tests/test_pods.py
+++ b/src/maasserver/forms/tests/test_pods.py
@@ -175,7 +175,8 @@ class TestPodForm(MAASTransactionServerTestCase):
         pod = form.save()
         self.assertEqual(pod.power_type, pod_info["type"])
         self.assertEqual(
-            pod.power_parameters["power_address"], pod_info["power_address"]
+            pod.get_power_parameters()["power_address"],
+            pod_info["power_address"],
         )
         self.assertEqual(pod.cores, 0)
         self.assertEqual(pod.memory, 0)
@@ -197,10 +198,11 @@ class TestPodForm(MAASTransactionServerTestCase):
         self.assertTrue(form.is_valid(), form._errors)
         pod = form.save()
         self.assertEqual(
-            pod_info["power_address"], pod.power_parameters["power_address"]
+            pod_info["power_address"],
+            pod.get_power_parameters()["power_address"],
         )
         self.assertEqual(
-            pod_info["power_pass"], pod.power_parameters["power_pass"]
+            pod_info["power_pass"], pod.get_power_parameters()["power_pass"]
         )
 
     def test_creates_pod_with_overcommit(self):
@@ -346,7 +348,7 @@ class TestPodForm(MAASTransactionServerTestCase):
         self.assertEqual(cpu_over_commit, pod.cpu_over_commit_ratio)
         self.assertEqual(memory_over_commit, pod.memory_over_commit_ratio)
         self.assertEqual(memory_over_commit, pod.memory_over_commit_ratio)
-        self.assertEqual(power_parameters, pod.power_parameters)
+        self.assertEqual(power_parameters, pod.get_power_parameters())
 
     def test_updates_existing_pod(self):
         zone = factory.make_Zone()
diff --git a/src/maasserver/migrations/maasserver/0290_migrate_node_power_parameters.py b/src/maasserver/migrations/maasserver/0290_migrate_node_power_parameters.py
new file mode 100644
index 0000000..aec854e
--- /dev/null
+++ b/src/maasserver/migrations/maasserver/0290_migrate_node_power_parameters.py
@@ -0,0 +1,63 @@
+# Generated by Django 3.2.12 on 2022-11-15 15:12
+
+from datetime import datetime
+
+from django.db import migrations
+
+from provisioningserver.drivers.power.registry import PowerDriverRegistry
+
+
+def move_secrets(apps, schema_editor):
+    BMC = apps.get_model("maasserver", "BMC")
+    Secret = apps.get_model("maasserver", "Secret")
+
+    now = datetime.utcnow()
+
+    bmc_secrets = {}
+
+    for bmc_id, power_type, power_parameters in BMC.objects.values_list(
+        "id", "power_type", "power_parameters"
+    ):
+
+        power_driver = PowerDriverRegistry.get_item(power_type)
+        if not power_driver:
+            continue
+
+        parameters = power_parameters.copy()
+
+        for power_parameter in power_parameters.keys():
+            setting = power_driver.get_setting(power_parameter)
+
+            if not setting.get("secret"):
+                continue
+
+            field_name = setting.get("name")
+            field_value = power_parameters.get(field_name)
+
+            bmc_secrets[bmc_id] = {
+                **bmc_secrets.get(bmc_id, {}),
+                **{field_name: field_value},
+            }
+
+            power_parameters.pop(field_name, None)
+
+        BMC.objects.filter(id=bmc_id).update(power_parameters=parameters)
+
+    Secret.objects.bulk_create(
+        Secret(
+            path=f"bmc/{bmc_id}/power-parameters",
+            value=secret,
+            created=now,
+            updated=now,
+        )
+        for bmc_id, secret in bmc_secrets.items()
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("maasserver", "0289_vault_secret"),
+    ]
+
+    operations = [migrations.RunPython(move_secrets)]
diff --git a/src/maasserver/models/bmc.py b/src/maasserver/models/bmc.py
index aab469c..b902288 100644
--- a/src/maasserver/models/bmc.py
+++ b/src/maasserver/models/bmc.py
@@ -70,7 +70,10 @@ from maasserver.utils.threads import deferToDatabase
 from metadataserver.enum import RESULT_TYPE
 from provisioningserver.drivers import SETTING_SCOPE
 from provisioningserver.drivers.pod import InterfaceAttachType
-from provisioningserver.drivers.power.registry import PowerDriverRegistry
+from provisioningserver.drivers.power.registry import (
+    PowerDriverRegistry,
+    sanitise_power_parameters,
+)
 from provisioningserver.enum import MACVLAN_MODE_CHOICES
 from provisioningserver.logger import get_maas_logger
 from provisioningserver.utils.constraints import LabeledConstraintMap
@@ -110,6 +113,47 @@ def get_ip_modes(requested_machine):
     return ip_modes
 
 
+def create_bmc(**kwargs):
+    power_type = kwargs.get("power_type", None)
+    power_parameters = kwargs.get("power_parameters", {})
+
+    power_parameters, secrets = sanitise_power_parameters(
+        power_type, power_parameters
+    )
+    kwargs["power_parameters"] = power_parameters
+
+    bmc = BMC.objects.create(**kwargs)
+
+    from maasserver.secrets import SecretManager
+
+    if secrets:
+        SecretManager().set_composite_secret(
+            "power-parameters", secrets, obj=bmc.as_bmc()
+        )
+    return bmc
+
+
+def get_or_create_bmc(**kwargs):
+    power_type = kwargs.get("power_type", None)
+    power_parameters = kwargs.get("power_parameters", {})
+
+    power_parameters, secrets = sanitise_power_parameters(
+        power_type, power_parameters
+    )
+    kwargs["power_parameters"] = power_parameters
+
+    bmc, created = BMC.objects.get_or_create(**kwargs)
+
+    if created and secrets:
+        from maasserver.secrets import SecretManager
+
+        SecretManager().set_composite_secret(
+            "power-parameters", secrets, obj=bmc.as_bmc()
+        )
+
+    return bmc, created
+
+
 class BaseBMCManager(Manager):
     """A utility to manage the collection of BMCs."""
 
@@ -333,6 +377,9 @@ class BMC(CleanSave, TimestampedModel):
     def delete(self):
         """Delete this BMC."""
         maaslog.info("%s: Deleting BMC", self)
+        from maasserver.secrets import SecretManager
+
+        SecretManager().delete_all_object_secrets(self)
         super().delete()
 
     def save(self, *args, **kwargs):
@@ -356,10 +403,35 @@ class BMC(CleanSave, TimestampedModel):
             else:
                 break
 
+    def get_power_parameters(self):
+        from maasserver.secrets import SecretManager
+
+        power_parameters = self.power_parameters or {}
+        return {
+            **power_parameters,
+            **SecretManager().get_composite_secret(
+                "power-parameters", obj=self.as_bmc(), default={}
+            ),
+        }
+
+    def set_power_parameters(self, power_parameters):
+        power_parameters, secrets = sanitise_power_parameters(
+            self.power_type, power_parameters
+        )
+        from maasserver.secrets import SecretManager
+
+        if secrets:
+            SecretManager().set_composite_secret(
+                "power-parameters", secrets, obj=self.as_bmc()
+            )
+        self.power_parameters = power_parameters
+
     def clean(self):
         """Update our ip_address if the address extracted from our power
         parameters has changed."""
-        new_ip = BMC.extract_ip_address(self.power_type, self.power_parameters)
+        new_ip = BMC.extract_ip_address(
+            self.power_type, self.get_power_parameters()
+        )
         current_ip = None if self.ip_address is None else self.ip_address.ip
         if new_ip != current_ip:
             if not new_ip:
@@ -685,7 +757,7 @@ class Pod(BMC):
     @property
     def tracked_project(self) -> str:
         """Return the project tracked by the Pod, or empty string."""
-        return self.power_parameters.get("project", "")
+        return self.get_power_parameters().get("project", "")
 
     @property
     def host(self):
@@ -907,7 +979,9 @@ class Pod(BMC):
             **kwargs,
         )
         machine.bmc = self
-        machine.instance_power_parameters = discovered_machine.power_parameters
+        machine.set_instance_power_parameters(
+            discovered_machine.power_parameters
+        )
         if not machine.hostname:
             machine.set_random_hostname()
         machine.save()
@@ -1125,7 +1199,7 @@ class Pod(BMC):
 
         # Sync power state and parameters for this machine always.
         existing_machine.power_state = discovered_machine.power_state
-        existing_machine.instance_power_parameters = (
+        existing_machine.set_instance_power_parameters(
             discovered_machine.power_parameters
         )
         existing_machine.cpu_count = discovered_machine.cores
@@ -1490,7 +1564,7 @@ class Pod(BMC):
             pool.pool_id: pool for pool in self.storage_pools.all()
         }
         possible_default = None
-        upgrade_default_pool = self.power_parameters.get(
+        upgrade_default_pool = self.get_power_parameters().get(
             "default_storage_pool"
         )
         for discovered_pool in discovered_storage_pools:
@@ -1524,7 +1598,7 @@ class Pod(BMC):
         if not self.default_storage_pool and possible_default:
             self.default_storage_pool = possible_default
             if upgrade_default_pool is not None:
-                self.power_parameters = self.power_parameters.copy()
+                self.power_parameters = self.get_power_parameters().copy()
                 self.power_parameters.pop("default_storage_pool", None)
             self.save()
         elif self.default_storage_pool in storage_pools_by_id.values():
@@ -1545,13 +1619,14 @@ class Pod(BMC):
         interfaces, and/or block devices that do not match the
         `discovered_pod` values will be removed.
         """
-        if self.power_type == "lxd" and "password" in self.power_parameters:
+        pod_power_parameters = self.get_power_parameters()
+        if self.power_type == "lxd" and "password" in pod_power_parameters:
             # ensure LXD trust_password is removed if it's there
             #
             # XXX copy and replace the whole attribute as the CleanSave base
             # class tracks which attributes to update on save via __setattr__,
             # so changing the value of the dict doesn't cause it to be updated.
-            power_params = self.power_parameters.copy()
+            power_params = pod_power_parameters.copy()
             del power_params["password"]
             self.power_parameters = power_params
         self.version = discovered_pod.version
@@ -1674,7 +1749,7 @@ class Pod(BMC):
         @transactional
         def gather_clients_and_machines(pod):
             machine_details = [
-                (machine.id, machine.power_parameters)
+                (machine.id, machine.get_power_parameters())
                 for machine in Machine.objects.filter(
                     bmc__id=pod.id
                 ).select_related("bmc")
diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
index ba2f710..b6a0590 100644
--- a/src/maasserver/models/node.py
+++ b/src/maasserver/models/node.py
@@ -182,7 +182,10 @@ from metadataserver.user_data import generate_user_data_for_status
 from provisioningserver.drivers.osystem import OperatingSystemRegistry
 from provisioningserver.drivers.pod import Capabilities
 from provisioningserver.drivers.power.ipmi import IPMI_BOOT_TYPE
-from provisioningserver.drivers.power.registry import PowerDriverRegistry
+from provisioningserver.drivers.power.registry import (
+    PowerDriverRegistry,
+    sanitise_power_parameters,
+)
 from provisioningserver.events import EVENT_DETAILS, EVENT_TYPES
 from provisioningserver.logger import get_maas_logger, LegacyLogger
 from provisioningserver.refresh.node_info_scripts import (
@@ -248,7 +251,7 @@ def get_bios_boot_from_bmc(bmc):
     """
     if bmc is None or bmc.power_type != "ipmi":
         return None
-    power_boot_type = bmc.power_parameters.get("power_boot_type")
+    power_boot_type = bmc.get_power_parameters().get("power_boot_type")
     if power_boot_type == IPMI_BOOT_TYPE.EFI:
         return "uefi"
     elif power_boot_type == IPMI_BOOT_TYPE.LEGACY:
@@ -1386,7 +1389,7 @@ class Node(CleanSave, TimestampedModel):
         current BMC, so if the BMC is a chassis, the configuration will apply
         to all connected nodes.
         """
-        from maasserver.models.bmc import BMC
+        from maasserver.models.bmc import BMC, create_bmc, get_or_create_bmc
 
         created_by_commissioning = from_commissioning
         old_bmc = self.bmc
@@ -1395,7 +1398,7 @@ class Node(CleanSave, TimestampedModel):
         )
         if power_type == self.power_type:
             if power_type == "manual":
-                self.bmc.power_parameters = bmc_params
+                self.bmc.set_power_parameters(bmc_params)
                 self.bmc.save()
             else:
                 existing_bmc = BMC.objects.filter(
@@ -1408,10 +1411,10 @@ class Node(CleanSave, TimestampedModel):
                         node.save()
                     self.bmc = existing_bmc
                 elif not existing_bmc:
-                    self.bmc.power_parameters = bmc_params
+                    self.bmc.set_power_parameters(bmc_params)
                     self.bmc.save()
         elif chassis:
-            self.bmc, _ = BMC.objects.get_or_create(
+            self.bmc, _ = get_or_create_bmc(
                 power_type=power_type,
                 power_parameters=bmc_params,
                 defaults={
@@ -1419,14 +1422,14 @@ class Node(CleanSave, TimestampedModel):
                 },
             )
         else:
-            self.bmc = BMC.objects.create(
+            self.bmc = create_bmc(
                 power_type=power_type,
                 power_parameters=bmc_params,
                 created_by_commissioning=created_by_commissioning,
             )
             self.bmc.save()
 
-        self.instance_power_parameters = node_params or {}
+        self.set_instance_power_parameters(node_params or {})
 
         # delete the old bmc if no node is connected to it
         if old_bmc and old_bmc != self.bmc and not old_bmc.node_set.exists():
@@ -1436,23 +1439,47 @@ class Node(CleanSave, TimestampedModel):
     def power_type(self):
         return "" if self.bmc is None else self.bmc.power_type
 
-    @property
-    def power_parameters(self):
+    def get_instance_power_parameters(self):
+        from maasserver.secrets import SecretManager
+
+        power_parameters = self.instance_power_parameters or {}
+        return {
+            **power_parameters,
+            **SecretManager().get_composite_secret(
+                "power-parameters", obj=self.as_node(), default={}
+            ),
+        }
+
+    def set_instance_power_parameters(self, power_parameters):
+        power_parameters, secrets = sanitise_power_parameters(
+            self.power_type, power_parameters
+        )
+        from maasserver.secrets import SecretManager
+
+        if secrets:
+            SecretManager().set_composite_secret(
+                "power-parameters", secrets, obj=self.as_node()
+            )
+        self.instance_power_parameters = power_parameters
+
+    def get_power_parameters(self):
         # Overlay instance power parameters over bmc power parameters.
-        instance_parameters = self.instance_power_parameters
+        instance_parameters = self.get_instance_power_parameters()
         if not instance_parameters:
             instance_parameters = {}
         bmc_parameters = {}
-        if self.bmc and self.bmc.power_parameters:
-            bmc_parameters = self.bmc.power_parameters
+        if self.bmc and (
+            bmc_power_parameters := self.bmc.get_power_parameters()
+        ):
+            bmc_parameters = bmc_power_parameters
         return {**bmc_parameters, **instance_parameters}
 
     @property
     def instance_name(self):
         """Return the name of the VM instance for this machine, or None."""
-        return self.instance_power_parameters.get(
+        return self.get_instance_power_parameters().get(
             "instance_name"
-        ) or self.instance_power_parameters.get(  # for LXD
+        ) or self.get_instance_power_parameters().get(  # for LXD
             "power_id"
         )  # for virsh
 
@@ -2994,7 +3021,7 @@ class Node(CleanSave, TimestampedModel):
             d.addCallback(
                 decompose_machine,
                 pod.power_type,
-                self.power_parameters,
+                self.get_power_parameters(),
                 pod_id=pod.id,
                 name=pod.name,
             )
@@ -3098,7 +3125,7 @@ class Node(CleanSave, TimestampedModel):
 
     def get_effective_power_parameters(self):
         """Return effective power parameters, including any defaults."""
-        power_params = self.power_parameters.copy()
+        power_params = self.get_power_parameters().copy()
 
         power_params.setdefault("system_id", self.system_id)
         # TODO: This default ought to be in the virsh template.
diff --git a/src/maasserver/models/tests/test_bmc.py b/src/maasserver/models/tests/test_bmc.py
index 183da8a..eb4b0b6 100644
--- a/src/maasserver/models/tests/test_bmc.py
+++ b/src/maasserver/models/tests/test_bmc.py
@@ -54,6 +54,7 @@ from maasserver.models.virtualmachine import (
 )
 from maasserver.models.vmcluster import VMCluster
 from maasserver.permissions import PodPermission
+from maasserver.secrets import SecretManager
 from maasserver.testing.factory import factory
 from maasserver.testing.fixtures import RBACEnabled
 from maasserver.testing.matchers import MatchesSetwiseWithAll
@@ -229,10 +230,12 @@ class TestBMC(MAASServerTestCase):
         sticky_ip = factory.make_StaticIPAddress(
             alloc_type=IPADDRESS_TYPE.STICKY, subnet=subnet
         )
-        bmc.power_parameters = {
-            "power_address": "protocol://%s:8080/path/to/thing#tag"
-            % (factory.ip_to_url_format(sticky_ip.ip))
-        }
+        bmc.set_power_parameters(
+            {
+                "power_address": "protocol://%s:8080/path/to/thing#tag"
+                % (factory.ip_to_url_format(sticky_ip.ip))
+            }
+        )
         bmc.save()
         self.assertEqual(sticky_ip.ip, bmc.ip_address.ip)
         self.assertEqual(subnet, bmc.ip_address.subnet)
@@ -318,10 +321,12 @@ class TestBMC(MAASServerTestCase):
         new_ip_address.delete()
         self.assertNotEqual(new_ip, old_ip)
 
-        bmc.power_parameters = {
-            "power_address": "protocol://%s:8080/path/to/thing#tag"
-            % (factory.ip_to_url_format(new_ip))
-        }
+        bmc.set_power_parameters(
+            {
+                "power_address": "protocol://%s:8080/path/to/thing#tag"
+                % (factory.ip_to_url_format(new_ip))
+            }
+        )
         bmc.save()
 
         # Check Machine has old IP address and kept same instance: machine_ip.
@@ -351,10 +356,12 @@ class TestBMC(MAASServerTestCase):
         machine, bmc, machine_ip = self.make_machine_and_bmc_differing_ips()
 
         # Now change the BMC's address to match machine's.
-        bmc.power_parameters = {
-            "power_address": "protocol://%s:8080/path/to/thing#tag"
-            % (factory.ip_to_url_format(machine_ip.ip))
-        }
+        bmc.set_power_parameters(
+            {
+                "power_address": "protocol://%s:8080/path/to/thing#tag"
+                % (factory.ip_to_url_format(machine_ip.ip))
+            }
+        )
         bmc.save()
 
         # Make sure BMC and Machine are using same StaticIPAddress instance.
@@ -374,6 +381,20 @@ class TestBMC(MAASServerTestCase):
         bmc.delete()
         self.assertEqual(0, StaticIPAddress.objects.filter(id=ip.id).count())
 
+    def test_delete_deletes_bmc_secrets(self):
+        bmc = factory.make_BMC()
+        secret_manager = SecretManager()
+        secret_manager.set_composite_secret(
+            "power-parameters", {"foo": "bar"}, obj=bmc
+        )
+
+        bmc.delete()
+        self.assertIsNone(
+            secret_manager.get_simple_secret(
+                "power-parameters", obj=bmc, default=None
+            )
+        )
+
     def test_delete_bmc_spares_non_orphaned_ip_address(self):
         machine, bmc, machine_ip = self.make_machine_and_bmc_with_shared_ip()
         bmc.delete()
@@ -616,6 +637,15 @@ class TestBMC(MAASServerTestCase):
             HasLength(len(non_routable_racks)),
         )
 
+    def test_get_power_params(self):
+        bmc = factory.make_BMC()
+        secret_manager = SecretManager()
+        secret_manager.set_composite_secret(
+            "power-parameters", {"foo": "bar"}, obj=bmc
+        )
+
+        self.assertEqual({"foo": "bar"}, bmc.get_power_parameters())
+
 
 class TestPodManager(MAASServerTestCase):
     def enable_rbac(self):
@@ -1023,7 +1053,7 @@ class TestPod(MAASServerTestCase, PodTestMixin):
         )
         self.patch(pod, "sync_machines")
         pod.sync(discovered, factory.make_User())
-        self.assertNotIn("password", pod.power_parameters)
+        self.assertNotIn("password", pod.get_power_parameters())
 
     def test_sync_pod_creates_new_machines_connected_to_default_vlan(self):
         discovered = self.make_discovered_pod()
@@ -1065,7 +1095,7 @@ class TestPod(MAASServerTestCase, PodTestMixin):
                 created_machine.power_state, discovered_machine.power_state
             )
             self.assertEqual(
-                created_machine.instance_power_parameters,
+                created_machine.get_instance_power_parameters(),
                 discovered_machine.power_parameters,
             )
             self.assertFalse(created_machine.dynamic)
@@ -1243,7 +1273,7 @@ class TestPod(MAASServerTestCase, PodTestMixin):
         self.assertEqual(
             discovered_default.name, pod.default_storage_pool.name
         )
-        self.assertEqual({}, pod.power_parameters)
+        self.assertEqual({}, pod.get_power_parameters())
 
     def test_sync_pod_sets_default_numanode(self):
         discovered_bdev = self.make_discovered_block_device()
@@ -1983,7 +2013,7 @@ class TestPod(MAASServerTestCase, PodTestMixin):
                 created_machine.power_state, discovered_machine.power_state
             )
             self.assertEqual(
-                created_machine.instance_power_parameters,
+                created_machine.get_instance_power_parameters(),
                 discovered_machine.power_parameters,
             )
             self.assertFalse(created_machine.dynamic)
@@ -3059,19 +3089,21 @@ class TestPodDelete(MAASTransactionServerTestCase, PodTestMixin):
             bmc_module, "getClientFromIdentifiers"
         ).return_value = client
         yield pod.async_delete(decompose=True)
+
+        power_parameters = yield deferToDatabase(pod.get_power_parameters)
         client.assert_has_calls(
             [
                 call(
                     DecomposeMachine,
                     type=pod.power_type,
-                    context=pod.power_parameters,
+                    context=power_parameters,
                     pod_id=pod.id,
                     name=pod.name,
                 ),
                 call(
                     DecomposeMachine,
                     type=pod.power_type,
-                    context=pod.power_parameters,
+                    context=power_parameters,
                     pod_id=pod.id,
                     name=pod.name,
                 ),
diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
index 88e5246..85ce521 100644
--- a/src/maasserver/models/tests/test_node.py
+++ b/src/maasserver/models/tests/test_node.py
@@ -1106,12 +1106,12 @@ class TestNode(MAASServerTestCase):
 
     def test_instance_name_virsh_vm(self):
         node = factory.make_Node()
-        node.instance_power_parameters = {"power_id": "vm"}
+        node.set_instance_power_parameters({"power_id": "vm"})
         self.assertEqual(node.instance_name, "vm")
 
     def test_instance_name_lxd_vm(self):
         node = factory.make_Node()
-        node.instance_power_parameters = {"instance_name": "vm"}
+        node.set_instance_power_parameters({"instance_name": "vm"})
         self.assertEqual(node.instance_name, "vm")
 
     def test_default_pool_for_machine(self):
@@ -6140,11 +6140,11 @@ class TestNodePowerParameters(MAASServerTestCase):
         )
         node.save()
         node = reload_object(node)
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
 
     def test_power_parameters_default(self):
         node = factory.make_Node(power_type=None)
-        self.assertEqual({}, node.power_parameters)
+        self.assertEqual({}, node.get_power_parameters())
         self.assertIsNone(node.bmc)
 
     def test_power_parameters_from_commissioning_not_new(self):
@@ -6215,9 +6215,9 @@ class TestNodePowerParameters(MAASServerTestCase):
         node.set_power_config("hmc", parameters)
         node.save()
         node = reload_object(node)
-        self.assertEqual(parameters, node.power_parameters)
-        self.assertEqual(node_parameters, node.instance_power_parameters)
-        self.assertEqual(bmc_parameters, node.bmc.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
+        self.assertEqual(node_parameters, node.get_instance_power_parameters())
+        self.assertEqual(bmc_parameters, node.bmc.get_power_parameters())
         self.assertEqual("hmc", node.bmc.power_type)
         self.assertEqual(node.power_type, node.bmc.power_type)
         self.assertEqual(ip_address, node.bmc.ip_address.ip)
@@ -6235,7 +6235,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         node.save()
         node = reload_object(node)
         self.assertEqual("manual", node.bmc.power_type)
-        self.assertEqual({}, node.bmc.power_parameters)
+        self.assertEqual({}, node.bmc.get_power_parameters())
         self.assertTrue(BMC.objects.filter(power_type="manual"))
 
     def test_power_type_does_not_create_new_bmc_for_already_manual(self):
@@ -6270,9 +6270,9 @@ class TestNodePowerParameters(MAASServerTestCase):
         node.set_power_config("virsh", parameters)
         node.save()
         node = reload_object(node)
-        self.assertEqual(parameters, node.power_parameters)
-        self.assertEqual(node_parameters, node.instance_power_parameters)
-        self.assertEqual(bmc_parameters, node.bmc.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
+        self.assertEqual(node_parameters, node.get_instance_power_parameters())
+        self.assertEqual(bmc_parameters, node.bmc.get_power_parameters())
         self.assertEqual("10.0.2.1", node.bmc.ip_address.ip)
 
     def test_unknown_power_parameter_stored_on_node(self):
@@ -6285,9 +6285,9 @@ class TestNodePowerParameters(MAASServerTestCase):
         node.set_power_config("hmc", parameters)
         node.save()
         node = reload_object(node)
-        self.assertEqual(parameters, node.power_parameters)
-        self.assertEqual(node_parameters, node.instance_power_parameters)
-        self.assertEqual(bmc_parameters, node.bmc.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
+        self.assertEqual(node_parameters, node.get_instance_power_parameters())
+        self.assertEqual(bmc_parameters, node.bmc.get_power_parameters())
 
     def test_none_chassis_bmc_doesnt_consolidate(self):
         for _ in range(3):
@@ -6308,9 +6308,11 @@ class TestNodePowerParameters(MAASServerTestCase):
             node.set_power_config("apc", parameters)
             node.save()
             node = reload_object(node)
-            self.assertEqual(parameters, node.power_parameters)
-            self.assertEqual(node_parameters, node.instance_power_parameters)
-            self.assertEqual(bmc_parameters, node.bmc.power_parameters)
+            self.assertEqual(parameters, node.get_power_parameters())
+            self.assertEqual(
+                node_parameters, node.get_instance_power_parameters()
+            )
+            self.assertEqual(bmc_parameters, node.bmc.get_power_parameters())
             self.assertEqual("apc", node.bmc.power_type)
             nodes.append(node)
 
@@ -6361,7 +6363,7 @@ class TestNodePowerParameters(MAASServerTestCase):
 
         # Set new BMC's values back to match original BMC, and make
         # sure the BMC count decreases as they consolidate.
-        parameters = nodes[0].power_parameters
+        parameters = nodes[0].get_power_parameters()
         parameters["power_id"] = factory.make_string()
         nodes[1].set_power_config(nodes[0].power_type, parameters)
         nodes[1].save()
@@ -6377,7 +6379,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address=ip_address)
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertEqual(ip_address, node.bmc.ip_address.ip)
 
     def test_power_parameters_unexpected_values_tolerated(self):
@@ -6385,7 +6387,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = {factory.make_string(): factory.make_string()}
         node.set_power_config("virsh", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertIsNone(node.bmc.ip_address)
 
     def test_power_parameters_blank_ip_address_tolerated(self):
@@ -6393,7 +6395,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address="")
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertIsNone(node.bmc.ip_address)
 
     def test_power_parameters_ip_address_reset(self):
@@ -6402,7 +6404,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address=ip_address)
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertEqual(ip_address, node.bmc.ip_address.ip)
 
         # StaticIPAddress can be changed after being set.
@@ -6410,7 +6412,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address=ip_address)
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertEqual(ip_address, node.bmc.ip_address.ip)
 
         # StaticIPAddress can be made None after being set.
@@ -6418,7 +6420,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address="")
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertIsNone(node.bmc.ip_address)
 
         # StaticIPAddress can be changed after being made None.
@@ -6426,7 +6428,7 @@ class TestNodePowerParameters(MAASServerTestCase):
         parameters = dict(power_address=ip_address)
         node.set_power_config("hmc", parameters)
         node.save()
-        self.assertEqual(parameters, node.power_parameters)
+        self.assertEqual(parameters, node.get_power_parameters())
         self.assertEqual(ip_address, node.bmc.ip_address.ip)
 
     def test_orphaned_bmcs_are_removed(self):
@@ -6650,9 +6652,9 @@ class TestDecomposeMachineTransactional(
         client.return_value = defer.succeed({"hints": hints})
         machine = factory.make_Node(**kwargs)
         machine.bmc = pod
-        machine.instance_power_parameters = {
-            "power_id": factory.make_name("power_id")
-        }
+        machine.set_instance_power_parameters(
+            {"power_id": factory.make_name("power_id")}
+        )
         return pod, machine, hints, client
 
     def test_delete_deletes_virtual_machine(self):
@@ -6675,7 +6677,7 @@ class TestDecomposeMachineTransactional(
             MockCalledOnceWith(
                 DecomposeMachine,
                 type=pod.power_type,
-                context=machine.power_parameters,
+                context=machine.get_power_parameters(),
                 pod_id=pod.id,
                 name=pod.name,
             ),
@@ -6724,7 +6726,7 @@ class TestDecomposeMachineTransactional(
             MockCalledOnceWith(
                 DecomposeMachine,
                 type=pod.power_type,
-                context=machine.power_parameters,
+                context=machine.get_power_parameters(),
                 pod_id=pod.id,
                 name=pod.name,
             ),
diff --git a/src/maasserver/rpc/tests/test_nodes.py b/src/maasserver/rpc/tests/test_nodes.py
index f39ce04..dee97b7 100644
--- a/src/maasserver/rpc/tests/test_nodes.py
+++ b/src/maasserver/rpc/tests/test_nodes.py
@@ -78,7 +78,7 @@ class TestCreateNode(MAASTransactionServerTestCase):
 
         self.assertEqual(
             (architecture, "manual", {}),
-            (node.architecture, node.power_type, node.power_parameters),
+            (node.architecture, node.power_type, node.get_power_parameters()),
         )
 
         # Node will not have an auto-generated name because migrations are
@@ -109,7 +109,7 @@ class TestCreateNode(MAASTransactionServerTestCase):
             (
                 node.architecture,
                 node.power_type,
-                node.power_parameters,
+                node.get_power_parameters(),
                 node.hostname,
             ),
         )
@@ -162,7 +162,7 @@ class TestCreateNode(MAASTransactionServerTestCase):
             (
                 node.architecture,
                 node.power_type,
-                node.power_parameters,
+                node.get_power_parameters(),
                 node.domain.id,
                 node.hostname,
             ),
@@ -241,7 +241,7 @@ class TestCreateNode(MAASTransactionServerTestCase):
         # Reload the object from the DB so that we're sure its power
         # parameters are being persisted.
         node = reload_object(node)
-        self.assertEqual(power_parameters, node.power_parameters)
+        self.assertEqual(power_parameters, node.get_power_parameters())
 
     def test_forces_generic_subarchitecture_if_missing(self):
         self.prepare_rack_rpc()
diff --git a/src/maasserver/secrets.py b/src/maasserver/secrets.py
index 0607581..c404358 100644
--- a/src/maasserver/secrets.py
+++ b/src/maasserver/secrets.py
@@ -3,7 +3,7 @@ from typing import Any, Literal, NamedTuple, Optional
 from django.db.models import Model
 from hvac.exceptions import InvalidPath
 
-from maasserver.models import Node, RootKey, Secret, VaultSecret
+from maasserver.models import BMC, Node, RootKey, Secret, VaultSecret
 from maasserver.vault import get_region_vault_client_if_enabled, VaultClient
 
 SIMPLE_SECRET_KEY = "secret"
@@ -15,7 +15,7 @@ class UnknownSecret(Exception):
     def __init__(self, name: str, obj: Optional[Model] = None):
         self.name = name
         self.obj = obj
-        message = f"Unkownn secret '{name}'"
+        message = f"Unknown secret '{name}'"
         if obj is not None:
             message += f" for object {type(obj)}"
         super().__init__(message)
@@ -43,8 +43,9 @@ class ModelSecret(NamedTuple):
 MODEL_SECRETS = {
     secret.model: secret
     for secret in (
-        ModelSecret(Node, "node", ["deploy-metadata"]),
+        ModelSecret(Node, "node", ["deploy-metadata", "power-parameters"]),
         ModelSecret(RootKey, "rootkey", ["material"]),
+        ModelSecret(BMC, "bmc", ["power-parameters"]),
     )
 }
 
diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
index 6b5cbc4..65414ca 100644
--- a/src/maasserver/testing/factory.py
+++ b/src/maasserver/testing/factory.py
@@ -532,7 +532,7 @@ class Factory(maastesting.factory.Factory):
                 ip_address = existing_static_ips[0]
             bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet)
             power_params = {
-                **node.power_parameters,
+                **node.get_power_parameters(),
                 "power_address": "qemu+ssh://user@%s/system"
                 % (factory.ip_to_url_format(bmc_ip_address)),
                 "power_id": factory.make_name("power_id"),
diff --git a/src/maasserver/triggers/tests/test_websocket_listener.py b/src/maasserver/triggers/tests/test_websocket_listener.py
index 0fe50c8..1506d30 100644
--- a/src/maasserver/triggers/tests/test_websocket_listener.py
+++ b/src/maasserver/triggers/tests/test_websocket_listener.py
@@ -4399,7 +4399,7 @@ class TestBMCListener(
 
         def update_node(system_id):
             node = Node.objects.get(system_id=system_id)
-            node.bmc.power_parameters = {"power_address": "5.6.7.8"}
+            node.bmc.set_power_parameters({"power_address": "5.6.7.8"})
             node.bmc.save()
 
         yield listener.startService()
diff --git a/src/maasserver/vmhost.py b/src/maasserver/vmhost.py
index 9ee1f73..05af032 100644
--- a/src/maasserver/vmhost.py
+++ b/src/maasserver/vmhost.py
@@ -53,7 +53,7 @@ def request_commissioning_results(pod):
                 pod.name,
                 pod.power_type,
                 node.system_id,
-                pod.power_parameters,
+                pod.get_power_parameters(),
                 token.consumer.key,
                 token.key,
                 token.secret,
diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py
index 6510006..87b575e 100644
--- a/src/maasserver/websockets/handlers/controller.py
+++ b/src/maasserver/websockets/handlers/controller.py
@@ -197,8 +197,8 @@ class ControllerHandler(NodeHandler):
             data["vlan_ids"] = vlan_ids
 
             # include certificate info if present
-            certificate = obj.power_parameters.get("certificate")
-            key = obj.power_parameters.get("key")
+            certificate = obj.get_power_parameters().get("certificate")
+            key = obj.get_power_parameters().get("key")
             if certificate and key:
                 cert = Certificate.from_pem(certificate, key)
                 data["certificate"] = dehydrate_certificate(cert)
diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
index 058c433..dd4e85c 100644
--- a/src/maasserver/websockets/handlers/machine.py
+++ b/src/maasserver/websockets/handlers/machine.py
@@ -435,8 +435,8 @@ class MachineHandler(NodeHandler):
             data["devices"] = sorted(devices, key=itemgetter("fqdn"))
 
             # include certificate info if present
-            certificate = obj.power_parameters.get("certificate")
-            key = obj.power_parameters.get("key")
+            certificate = obj.get_power_parameters().get("certificate")
+            key = obj.get_power_parameters().get("key")
             if certificate and key:
                 cert = Certificate.from_pem(certificate, key)
                 data["certificate"] = dehydrate_certificate(cert)
diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
index 73a3862..218a113 100644
--- a/src/maasserver/websockets/handlers/node.py
+++ b/src/maasserver/websockets/handlers/node.py
@@ -448,7 +448,7 @@ class NodeHandler(TimestampedModelHandler):
 
                 data["power_type"] = obj.power_type
                 data["power_parameters"] = self.dehydrate_power_parameters(
-                    obj.power_parameters
+                    obj.get_power_parameters()
                 )
                 data["power_bmc_node_count"] = (
                     obj.bmc.node_set.count() if (obj.bmc is not None) else 0
diff --git a/src/maasserver/websockets/handlers/tests/test_controller.py b/src/maasserver/websockets/handlers/tests/test_controller.py
index b7832ee..683cac2 100644
--- a/src/maasserver/websockets/handlers/tests/test_controller.py
+++ b/src/maasserver/websockets/handlers/tests/test_controller.py
@@ -169,7 +169,7 @@ class TestControllerHandler(MAASServerTestCase):
         # and slowing down the client waiting for the response.
         self.assertEqual(
             queries,
-            31,
+            33,
             "Number of queries has changed; make sure this is expected.",
         )
 
diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
index bf3a453..e8045c9 100644
--- a/src/maasserver/websockets/handlers/tests/test_machine.py
+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
@@ -342,7 +342,7 @@ class TestMachineHandler(MAASServerTestCase):
             "owner": handler.dehydrate_owner(node.owner),
             "parent": node.parent,
             "power_parameters": handler.dehydrate_power_parameters(
-                node.power_parameters
+                node.get_power_parameters()
             ),
             "power_bmc_node_count": node.bmc.node_set.count()
             if (node.bmc is not None)
@@ -895,7 +895,7 @@ class TestMachineHandler(MAASServerTestCase):
         queries, _ = count_queries(handler.get, {"system_id": node.system_id})
         self.assertEqual(
             queries,
-            54,
+            60,
             "Number of queries has changed; make sure this is expected.",
         )
 
diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py
index 4669ee4..0387813 100644
--- a/src/metadataserver/api.py
+++ b/src/metadataserver/api.py
@@ -358,10 +358,10 @@ def store_node_power_parameters(node, request):
         except ValueError:
             raise MAASAPIBadRequest("Failed to parse JSON power_parameters")
     else:
-        power_parameters = node.power_parameters
+        power_parameters = node.get_power_parameters()
     if power_type == "redfish":
         power_parameters = {
-            **node.instance_power_parameters,
+            **node.get_instance_power_parameters(),
             **power_parameters,
         }
     node.set_power_config(
diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py
index d6c388b..4a1fd29 100644
--- a/src/metadataserver/tests/test_api.py
+++ b/src/metadataserver/tests/test_api.py
@@ -3102,7 +3102,7 @@ class TestCommissioningAPI(MAASServerTestCase):
         )
         node = reload_object(node)
         self.assertEqual("mscm", node.power_type)
-        self.assertNotEqual(params, node.power_parameters)
+        self.assertNotEqual(params, node.get_power_parameters())
 
     def test_signal_refuses_bad_power_type(self):
         node = factory.make_Node(
@@ -3134,7 +3134,7 @@ class TestCommissioningAPI(MAASServerTestCase):
         )
         node = reload_object(node)
         self.assertEqual("ipmi", node.power_type)
-        self.assertEqual(params, node.power_parameters)
+        self.assertEqual(params, node.get_power_parameters())
 
     def test_signal_power_type_lower_case_works(self):
         node = factory.make_Node(
@@ -3153,7 +3153,7 @@ class TestCommissioningAPI(MAASServerTestCase):
             http.client.OK, response.status_code, response.content
         )
         node = reload_object(node)
-        self.assertEqual(params, node.power_parameters)
+        self.assertEqual(params, node.get_power_parameters())
 
     def test_signal_invalid_power_parameters(self):
         node = factory.make_Node(
@@ -4103,7 +4103,7 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         self.node.set_power_config("", {})
         store_node_power_parameters(self.node, self.request)
         self.assertEqual("", self.node.power_type)
-        self.assertEqual({}, self.node.power_parameters)
+        self.assertEqual({}, self.node.get_power_parameters())
         self.save.assert_has_calls([])
 
     def test_power_type_redfish(self):
@@ -4116,8 +4116,8 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         store_node_power_parameters(self.node, self.request)
         self.assertEqual("redfish", self.node.power_type)
         self.assertEqual(
-            {**power_parameters, **self.node.instance_power_parameters},
-            self.node.power_parameters,
+            {**power_parameters, **self.node.get_instance_power_parameters()},
+            self.node.get_power_parameters(),
         )
         self.save.assert_has_calls([])
 
@@ -4127,7 +4127,8 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         store_node_power_parameters(self.node, self.request)
         self.assertEqual("redfish", self.node.power_type)
         self.assertEqual(
-            self.node.instance_power_parameters, self.node.power_parameters
+            self.node.get_instance_power_parameters(),
+            self.node.get_power_parameters(),
         )
         self.save.assert_has_calls([])
 
@@ -4140,9 +4141,10 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         }
         store_node_power_parameters(self.node, self.request)
         self.assertEqual("redfish", self.node.power_type)
-        self.assertEqual(self.node.power_parameters, power_parameters)
+        self.assertEqual(self.node.get_power_parameters(), power_parameters)
         self.assertEqual(
-            self.node.instance_power_parameters, self.node.power_parameters
+            self.node.get_instance_power_parameters(),
+            self.node.get_power_parameters(),
         )
         self.save.assert_called_once_with()
 
@@ -4154,7 +4156,7 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         self.request.POST = {"power_type": "virsh"}
         store_node_power_parameters(self.node, self.request)
         self.assertEqual(self.node.power_type, "virsh")
-        self.assertEqual({"some": "param"}, self.node.power_parameters)
+        self.assertEqual({"some": "param"}, self.node.get_power_parameters())
         self.save.assert_called_once_with()
 
     def test_power_type_set_with_parameters(self):
@@ -4168,7 +4170,7 @@ class TestStoreNodeParameters(APITestCase.ForUser):
         }
         store_node_power_parameters(self.node, self.request)
         self.assertEqual(power_type, self.node.power_type)
-        self.assertEqual(power_parameters, self.node.power_parameters)
+        self.assertEqual(power_parameters, self.node.get_power_parameters())
         self.assertTrue(self.node.bmc.created_by_commissioning)
         self.save.assert_called_once_with()
 
diff --git a/src/provisioningserver/drivers/__init__.py b/src/provisioningserver/drivers/__init__.py
index da0c9b2..e4b2f66 100644
--- a/src/provisioningserver/drivers/__init__.py
+++ b/src/provisioningserver/drivers/__init__.py
@@ -119,6 +119,7 @@ def make_setting_field(
     default=None,
     required=False,
     scope=SETTING_SCOPE.BMC,
+    secret=False,
 ):
     """Helper function for building a JSON setting parameters field.
 
@@ -140,6 +141,9 @@ def make_setting_field(
     :param scope: 'bmc' or 'node' - Whether value is bmc or node specific.
         Defaults to 'bmc'.
     :type scope: string
+    :param secret: True or False - Whether value should be stored securely.
+        Defaults to False
+    :type secret: boolean
     """
     if field_type not in (
         "string",
@@ -164,6 +168,7 @@ def make_setting_field(
         "choices": choices,
         "default": default,
         "scope": scope,
+        "secret": secret,
     }
 
 
diff --git a/src/provisioningserver/drivers/pod/lxd.py b/src/provisioningserver/drivers/pod/lxd.py
index ed46294..0181eaf 100644
--- a/src/provisioningserver/drivers/pod/lxd.py
+++ b/src/provisioningserver/drivers/pod/lxd.py
@@ -208,6 +208,7 @@ class LXDPodDriver(PodDriver):
             "LXD password (optional)",
             required=False,
             field_type="password",
+            secret=True,
         ),
         make_setting_field(
             "certificate",
@@ -219,6 +220,7 @@ class LXDPodDriver(PodDriver):
             "LXD private key (optional)",
             required=False,
             field_type="password",
+            secret=True,
         ),
     ]
     ip_extractor = make_ip_extractor(
diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py
index ffbbaf7..c490cbc 100644
--- a/src/provisioningserver/drivers/pod/virsh.py
+++ b/src/provisioningserver/drivers/pod/virsh.py
@@ -1285,6 +1285,7 @@ class VirshPodDriver(PodDriver):
             "Password (optional)",
             required=False,
             field_type="password",
+            secret=True,
         ),
         make_setting_field(
             "power_id", "Virsh VM ID", scope=SETTING_SCOPE.NODE, required=True
diff --git a/src/provisioningserver/drivers/power/amt.py b/src/provisioningserver/drivers/power/amt.py
index 300e160..cddebdd 100644
--- a/src/provisioningserver/drivers/power/amt.py
+++ b/src/provisioningserver/drivers/power/amt.py
@@ -51,7 +51,7 @@ class AMTPowerDriver(PowerDriver):
     description = "Intel AMT"
     settings = [
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
         make_setting_field("power_address", "Power address", required=True),
     ]
diff --git a/src/provisioningserver/drivers/power/dli.py b/src/provisioningserver/drivers/power/dli.py
index d5bd480..05deec7 100644
--- a/src/provisioningserver/drivers/power/dli.py
+++ b/src/provisioningserver/drivers/power/dli.py
@@ -38,7 +38,7 @@ class DLIPowerDriver(PowerDriver):
         make_setting_field("power_address", "Power address", required=True),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
     ]
     ip_extractor = make_ip_extractor("power_address")
diff --git a/src/provisioningserver/drivers/power/hmc.py b/src/provisioningserver/drivers/power/hmc.py
index 4c773ef..f397f19 100644
--- a/src/provisioningserver/drivers/power/hmc.py
+++ b/src/provisioningserver/drivers/power/hmc.py
@@ -41,7 +41,7 @@ class HMCPowerDriver(PowerDriver):
         make_setting_field("power_address", "IP for HMC", required=True),
         make_setting_field("power_user", "HMC username"),
         make_setting_field(
-            "power_pass", "HMC password", field_type="password"
+            "power_pass", "HMC password", field_type="password", secret=True
         ),
         make_setting_field(
             "server_name",
diff --git a/src/provisioningserver/drivers/power/hmcz.py b/src/provisioningserver/drivers/power/hmcz.py
index 2645a53..f5072eb 100644
--- a/src/provisioningserver/drivers/power/hmcz.py
+++ b/src/provisioningserver/drivers/power/hmcz.py
@@ -48,7 +48,11 @@ class HMCZPowerDriver(PowerDriver):
         make_setting_field("power_address", "HMC Address", required=True),
         make_setting_field("power_user", "HMC username", required=True),
         make_setting_field(
-            "power_pass", "HMC password", field_type="password", required=True
+            "power_pass",
+            "HMC password",
+            field_type="password",
+            required=True,
+            secret=True,
         ),
         make_setting_field(
             "power_partition_name",
diff --git a/src/provisioningserver/drivers/power/ipmi.py b/src/provisioningserver/drivers/power/ipmi.py
index 70201e8..09f3295 100644
--- a/src/provisioningserver/drivers/power/ipmi.py
+++ b/src/provisioningserver/drivers/power/ipmi.py
@@ -276,9 +276,11 @@ class IPMIPowerDriver(PowerDriver):
         make_setting_field("power_address", "IP address", required=True),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
+        ),
+        make_setting_field(
+            "k_g", "K_g BMC key", field_type="password", secret=True
         ),
-        make_setting_field("k_g", "K_g BMC key", field_type="password"),
         make_setting_field(
             "cipher_suite_id",
             "Cipher Suite ID",
diff --git a/src/provisioningserver/drivers/power/moonshot.py b/src/provisioningserver/drivers/power/moonshot.py
index c5f21c5..44e45e3 100644
--- a/src/provisioningserver/drivers/power/moonshot.py
+++ b/src/provisioningserver/drivers/power/moonshot.py
@@ -31,7 +31,7 @@ class MoonshotIPMIPowerDriver(PowerDriver):
         make_setting_field("power_address", "Power address", required=True),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
         make_setting_field(
             "power_hwaddress",
diff --git a/src/provisioningserver/drivers/power/mscm.py b/src/provisioningserver/drivers/power/mscm.py
index 17862c8..1c93681 100644
--- a/src/provisioningserver/drivers/power/mscm.py
+++ b/src/provisioningserver/drivers/power/mscm.py
@@ -63,7 +63,10 @@ class MSCMPowerDriver(PowerDriver):
         ),
         make_setting_field("power_user", "MSCM CLI API user"),
         make_setting_field(
-            "power_pass", "MSCM CLI API password", field_type="password"
+            "power_pass",
+            "MSCM CLI API password",
+            field_type="password",
+            secret=True,
         ),
         make_setting_field(
             "node_id",
diff --git a/src/provisioningserver/drivers/power/msftocs.py b/src/provisioningserver/drivers/power/msftocs.py
index 2f8c8fe..8313f45 100644
--- a/src/provisioningserver/drivers/power/msftocs.py
+++ b/src/provisioningserver/drivers/power/msftocs.py
@@ -43,7 +43,7 @@ class MicrosoftOCSPowerDriver(PowerDriver):
         make_setting_field("power_port", "Power port"),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
         make_setting_field(
             "blade_id",
diff --git a/src/provisioningserver/drivers/power/nova.py b/src/provisioningserver/drivers/power/nova.py
index 0eb26b4..fc9d7b1 100644
--- a/src/provisioningserver/drivers/power/nova.py
+++ b/src/provisioningserver/drivers/power/nova.py
@@ -48,7 +48,11 @@ class NovaPowerDriver(PowerDriver):
         make_setting_field("os_tenantname", "Tenant name", required=True),
         make_setting_field("os_username", "Username", required=True),
         make_setting_field(
-            "os_password", "Password", field_type="password", required=True
+            "os_password",
+            "Password",
+            field_type="password",
+            required=True,
+            secret=True,
         ),
         make_setting_field("os_authurl", "Auth URL", required=True),
     ]
diff --git a/src/provisioningserver/drivers/power/openbmc.py b/src/provisioningserver/drivers/power/openbmc.py
index b855399..2ffe406 100644
--- a/src/provisioningserver/drivers/power/openbmc.py
+++ b/src/provisioningserver/drivers/power/openbmc.py
@@ -51,6 +51,7 @@ class OpenBMCPowerDriver(PowerDriver):
             "OpenBMC password",
             field_type="password",
             required=True,
+            secret=True,
         ),
     ]
     ip_extractor = make_ip_extractor("power_address")
diff --git a/src/provisioningserver/drivers/power/proxmox.py b/src/provisioningserver/drivers/power/proxmox.py
index 9c427d4..a7b5635 100644
--- a/src/provisioningserver/drivers/power/proxmox.py
+++ b/src/provisioningserver/drivers/power/proxmox.py
@@ -47,12 +47,14 @@ class ProxmoxPowerDriver(WebhookPowerDriver):
             "Proxmox password, required if a token name and secret aren't "
             "given",
             field_type="password",
+            secret=True,
         ),
         make_setting_field("power_token_name", "Proxmox API token name"),
         make_setting_field(
             "power_token_secret",
             "Proxmox API token secret",
             field_type="password",
+            secret=True,
         ),
         make_setting_field(
             "power_vm_name", "Node ID", scope=SETTING_SCOPE.NODE, required=True
diff --git a/src/provisioningserver/drivers/power/recs.py b/src/provisioningserver/drivers/power/recs.py
index 8264471..ffddcbc 100644
--- a/src/provisioningserver/drivers/power/recs.py
+++ b/src/provisioningserver/drivers/power/recs.py
@@ -215,7 +215,7 @@ class RECSPowerDriver(PowerDriver):
         make_setting_field("power_port", "Power port"),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
     ]
     ip_extractor = make_ip_extractor("power_address")
diff --git a/src/provisioningserver/drivers/power/redfish.py b/src/provisioningserver/drivers/power/redfish.py
index 19d9ecd..f554a22 100644
--- a/src/provisioningserver/drivers/power/redfish.py
+++ b/src/provisioningserver/drivers/power/redfish.py
@@ -166,6 +166,7 @@ class RedfishPowerDriver(RedfishPowerDriverBase):
             "Redfish password",
             field_type="password",
             required=True,
+            secret=True,
         ),
         make_setting_field("node_id", "Node ID", scope=SETTING_SCOPE.NODE),
     ]
diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py
index 2cedfaf..4ffdae3 100644
--- a/src/provisioningserver/drivers/power/registry.py
+++ b/src/provisioningserver/drivers/power/registry.py
@@ -82,3 +82,37 @@ for driver in power_drivers:
 # Pod drivers are also power drivers.
 for driver_name, driver in PodDriverRegistry:
     PowerDriverRegistry.register_item(driver_name, driver)
+
+
+def sanitise_power_parameters(power_type, power_parameters):
+    """Performs extraction of sensitive parameters and returns them separately.
+    Extraction relies on a `secret` flag of the power parameters property.
+
+    :param power_type: BMC power driver type
+    :param power_parameters: BMC power parameters
+    """
+    power_driver = PowerDriverRegistry.get_item(power_type)
+
+    if not power_driver:
+        return power_parameters, {}
+
+    parameters = power_parameters.copy()
+    secrets = {}
+
+    for power_parameter in power_parameters.keys():
+        setting = power_driver.get_setting(power_parameter)
+        if not setting:
+            continue
+        if not setting.get("secret"):
+            continue
+
+        field_name = setting.get("name")
+        field_value = power_parameters.get(field_name)
+
+        secrets = {
+            **secrets,
+            **{field_name: field_value},
+        }
+        parameters.pop(field_name, None)
+
+    return parameters, secrets
diff --git a/src/provisioningserver/drivers/power/seamicro.py b/src/provisioningserver/drivers/power/seamicro.py
index 83ec5c6..aa44929 100644
--- a/src/provisioningserver/drivers/power/seamicro.py
+++ b/src/provisioningserver/drivers/power/seamicro.py
@@ -49,7 +49,7 @@ class SeaMicroPowerDriver(PowerDriver):
         make_setting_field("power_address", "Power address", required=True),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
         make_setting_field(
             "power_control",
diff --git a/src/provisioningserver/drivers/power/ucsm.py b/src/provisioningserver/drivers/power/ucsm.py
index 948ec3c..43c4b5c 100644
--- a/src/provisioningserver/drivers/power/ucsm.py
+++ b/src/provisioningserver/drivers/power/ucsm.py
@@ -39,7 +39,7 @@ class UCSMPowerDriver(PowerDriver):
         make_setting_field("power_address", "URL for XML API", required=True),
         make_setting_field("power_user", "API user"),
         make_setting_field(
-            "power_pass", "API password", field_type="password"
+            "power_pass", "API password", field_type="password", secret=True
         ),
     ]
     ip_extractor = make_ip_extractor(
diff --git a/src/provisioningserver/drivers/power/vmware.py b/src/provisioningserver/drivers/power/vmware.py
index 63799e7..21929a0 100644
--- a/src/provisioningserver/drivers/power/vmware.py
+++ b/src/provisioningserver/drivers/power/vmware.py
@@ -60,6 +60,7 @@ class VMwarePowerDriver(PowerDriver):
             "VMware password",
             field_type="password",
             required=True,
+            secret=True,
         ),
         make_setting_field(
             "power_port", "VMware API port (optional)", required=False
diff --git a/src/provisioningserver/drivers/power/webhook.py b/src/provisioningserver/drivers/power/webhook.py
index 11a92a9..ac61510 100644
--- a/src/provisioningserver/drivers/power/webhook.py
+++ b/src/provisioningserver/drivers/power/webhook.py
@@ -66,12 +66,13 @@ class WebhookPowerDriver(PowerDriver):
         ),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
         make_setting_field(
             "power_token",
             "Power token, will be used in place of power_user and power_pass",
             field_type="password",
+            secret=True,
         ),
         make_setting_field(
             "power_verify_ssl",
diff --git a/src/provisioningserver/drivers/power/wedge.py b/src/provisioningserver/drivers/power/wedge.py
index f214032..9f723fb 100644
--- a/src/provisioningserver/drivers/power/wedge.py
+++ b/src/provisioningserver/drivers/power/wedge.py
@@ -33,7 +33,7 @@ class WedgePowerDriver(PowerDriver):
         make_setting_field("power_address", "IP address", required=True),
         make_setting_field("power_user", "Power user"),
         make_setting_field(
-            "power_pass", "Power password", field_type="password"
+            "power_pass", "Power password", field_type="password", secret=True
         ),
     ]
     ip_extractor = make_ip_extractor("power_address")
diff --git a/src/provisioningserver/drivers/tests/test_base.py b/src/provisioningserver/drivers/tests/test_base.py
index a7440e7..0b1a67b 100644
--- a/src/provisioningserver/drivers/tests/test_base.py
+++ b/src/provisioningserver/drivers/tests/test_base.py
@@ -388,6 +388,7 @@ class TestMakeSettingField(MAASTestCase):
                 "default": default,
                 "required": True,
                 "scope": SETTING_SCOPE.NODE,
+                "secret": False,
             },
             setting,
         )
-- 
Mailing list: https://launchpad.net/~sts-sponsors
Post to     : sts-sponsors@lists.launchpad.net
Unsubscribe : https://launchpad.net/~sts-sponsors
More help   : https://help.launchpad.net/ListHelp

Reply via email to