mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	Adding extension support for Baremetal MaaS
This commit is contained in:
		
							parent
							
								
									ec533cd24d
								
							
						
					
					
						commit
						e5c5a64b4e
					
				
							
								
								
									
										1
									
								
								debian/cloudstack-management.install
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								debian/cloudstack-management.install
									
									
									
									
										vendored
									
									
								
							| @ -24,6 +24,7 @@ | |||||||
| /etc/cloudstack/management/config.json | /etc/cloudstack/management/config.json | ||||||
| /etc/cloudstack/extensions/Proxmox/proxmox.sh | /etc/cloudstack/extensions/Proxmox/proxmox.sh | ||||||
| /etc/cloudstack/extensions/HyperV/hyperv.py | /etc/cloudstack/extensions/HyperV/hyperv.py | ||||||
|  | /etc/cloudstack/extensions/MaaS/maas.py | ||||||
| /etc/default/cloudstack-management | /etc/default/cloudstack-management | ||||||
| /etc/security/limits.d/cloudstack-limits.conf | /etc/security/limits.d/cloudstack-limits.conf | ||||||
| /etc/sudoers.d/cloudstack | /etc/sudoers.d/cloudstack | ||||||
|  | |||||||
							
								
								
									
										209
									
								
								extensions/MaaS/maas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								extensions/MaaS/maas.py
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user