This is an automated email from the ASF dual-hosted git repository.

harikrishna pushed a commit to branch ExtensionMaaS
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit e5c5a64b4eabe73e65a0f7deb387c7dff0946cf6
Author: Harikrishna Patnala <harikrishna.patn...@gmail.com>
AuthorDate: Tue Sep 9 10:45:23 2025 +0530

    Adding extension support for Baremetal MaaS
---
 debian/cloudstack-management.install |   1 +
 extensions/MaaS/maas.py              | 209 +++++++++++++++++++++++++++++++++++
 2 files changed, 210 insertions(+)

diff --git a/debian/cloudstack-management.install 
b/debian/cloudstack-management.install
index b2a32bd93c1..befc7049c30 100644
--- a/debian/cloudstack-management.install
+++ b/debian/cloudstack-management.install
@@ -24,6 +24,7 @@
 /etc/cloudstack/management/config.json
 /etc/cloudstack/extensions/Proxmox/proxmox.sh
 /etc/cloudstack/extensions/HyperV/hyperv.py
+/etc/cloudstack/extensions/MaaS/maas.py
 /etc/default/cloudstack-management
 /etc/security/limits.d/cloudstack-limits.conf
 /etc/sudoers.d/cloudstack
diff --git a/extensions/MaaS/maas.py b/extensions/MaaS/maas.py
new file mode 100644
index 00000000000..93316bf0f56
--- /dev/null
+++ b/extensions/MaaS/maas.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import sys
+import json
+from requests_oauthlib import OAuth1Session
+
+
+def fail(message):
+    print(json.dumps({"error": message}))
+    sys.exit(1)
+
+
+def succeed(data):
+    print(json.dumps(data))
+    sys.exit(0)
+
+
+class MaasManager:
+    def __init__(self, config_path):
+        self.config_path = config_path
+        self.data = self.parse_json()
+        self.session = self.init_session()
+
+    def parse_json(self):
+        try:
+            with open(self.config_path, "r") as f:
+                json_data = json.load(f)
+
+            extension = json_data.get("externaldetails", {}).get("extension", 
{})
+            host = json_data.get("externaldetails", {}).get("host", {})
+
+            endpoint = host.get("endpoint") or extension.get("endpoint")
+            apikey = host.get("apikey") or extension.get("apikey")
+            distro_series = host.get("distro_series") or 
extension.get("distro_series") or "ubuntu"
+
+            if not endpoint or not apikey:
+                fail("Missing MAAS endpoint or apikey")
+
+            # normalize endpoint
+            if not endpoint.startswith("http://";) and not 
endpoint.startswith("https://";):
+                endpoint = "http://"; + endpoint
+            endpoint = endpoint.rstrip("/")
+
+            # split api key
+            parts = apikey.split(":")
+            if len(parts) != 3:
+                fail("Invalid apikey format. Expected consumer:token:secret")
+
+            consumer, token, secret = parts
+            return {
+                "endpoint": endpoint,
+                "consumer": consumer,
+                "token": token,
+                "secret": secret,
+                "distro_series": distro_series,
+                "system_id": json_data.get("cloudstack.vm.details", 
{}).get("details", {}).get("maas_system_id", ""),
+                "vm_name": json_data.get("cloudstack.vm.details", 
{}).get("name", ""),
+                "memory": json_data.get("cloudstack.vm.details", 
{}).get("minRam", ""),
+                "cpus": json_data.get("cloudstack.vm.details", {}).get("cpus", 
""),
+                "nics": json_data.get("cloudstack.vm.details", {}).get("nics", 
[]),
+            }
+        except Exception as e:
+            fail(f"Error parsing JSON: {str(e)}")
+
+    def init_session(self):
+        return OAuth1Session(
+            self.data["consumer"],
+            resource_owner_key=self.data["token"],
+            resource_owner_secret=self.data["secret"],
+        )
+
+    def call_maas(self, method, path, data=None):
+        if not path.startswith("/"):
+            path = "/" + path
+        url = f"{self.data['endpoint']}:5240/MAAS/api/2.0{path}"
+        resp = self.session.request(method, url, data=data)
+        if not resp.ok:
+            fail(f"MAAS API error: {resp.status_code} {resp.text}")
+        try:
+            return resp.json() if resp.text else {}
+        except ValueError:
+            return {}
+
+    def prepare(self):
+        machines = self.call_maas("GET", "/machines/")
+        ready = [m for m in machines if m.get("status_name") == "Ready"]
+        if not ready:
+            fail("No Ready machines available")
+
+        system = ready[0]
+        system_id = system["system_id"]
+        mac = system.get("interface_set", [{}])[0].get("mac_address")
+
+        if not mac:
+            fail("No MAC address found")
+
+        # Load original JSON so we can update nics
+        with open(self.config_path, "r") as f:
+            json_data = json.load(f)
+
+        if json_data.get("cloudstack.vm.details", {}).get("nics"):
+            json_data["cloudstack.vm.details"]["nics"][0]["mac"] = mac
+
+        result = {
+            "nics": json_data["cloudstack.vm.details"]["nics"],
+            "details": {"External:mac_address": mac, "maas_system_id": 
system_id},
+        }
+        succeed(result)
+
+    def create(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for create")
+        self.call_maas(
+            "POST",
+            f"/machines/{sysid}/",
+            {"op": "deploy", "distro_series": self.data["distro_series"]},
+        )
+        succeed({"status": "success", "message": f"Instance created with 
{self.data['distro_series']}"})
+
+    def delete(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for delete")
+        self.call_maas("POST", f"/machines/{sysid}/", {"op": "release"})
+        succeed({"status": "success", "message": "Instance deleted"})
+
+    def start(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for start")
+        self.call_maas("POST", f"/machines/{sysid}/", {"op": "power_on"})
+        succeed({"status": "success", "power_state": "PowerOn"})
+
+    def stop(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for stop")
+        self.call_maas("POST", f"/machines/{sysid}/", {"op": "power_off"})
+        succeed({"status": "success", "power_state": "PowerOff"})
+
+    def reboot(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for reboot")
+        self.call_maas("POST", f"/machines/{sysid}/", {"op": "power_cycle"})
+        succeed({"status": "success", "power_state": "PowerOn"})
+
+    def status(self):
+        sysid = self.data.get("system_id")
+        if not sysid:
+            fail("system_id missing for status")
+        resp = self.call_maas("GET", f"/machines/{sysid}/")
+        state = resp.get("power_state", "")
+        if state == "on":
+            mapped = "PowerOn"
+        elif state == "off":
+            mapped = "PowerOff"
+        else:
+            mapped = "PowerUnknown"
+        succeed({"status": "success", "power_state": mapped})
+
+
+def main():
+    if len(sys.argv) < 3:
+        fail("Usage: maas.py <action> <json-file-path>")
+
+    action = sys.argv[1].lower()
+    json_file = sys.argv[2]
+
+    try:
+        manager = MaasManager(json_file)
+    except FileNotFoundError:
+        fail(f"JSON file not found: {json_file}")
+
+    actions = {
+        "prepare": manager.prepare,
+        "create": manager.create,
+        "delete": manager.delete,
+        "start": manager.start,
+        "stop": manager.stop,
+        "reboot": manager.reboot,
+        "status": manager.status,
+    }
+
+    if action not in actions:
+        fail("Invalid action")
+
+    actions[action]()
+
+
+if __name__ == "__main__":
+    main()

Reply via email to