Initial commit
This commit is contained in:
291
skills/iot-edge-module/scripts/detect_project_structure.py
Normal file
291
skills/iot-edge-module/scripts/detect_project_structure.py
Normal file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Detect IoT Edge project structure by scanning for existing patterns.
|
||||
|
||||
This script auto-detects:
|
||||
- Modules base path
|
||||
- Contracts project path and name
|
||||
- Deployment manifests location
|
||||
- Project namespace
|
||||
- Container registry (from existing manifests)
|
||||
- NuGet feed URL (from existing Dockerfiles)
|
||||
|
||||
Returns JSON with detected configuration or empty fields if not found.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
def find_files_recursive(root_path: Path, pattern: str) -> list[Path]:
|
||||
"""Find files matching glob pattern recursively, excluding test directories."""
|
||||
files = list(root_path.rglob(pattern))
|
||||
|
||||
# Always filter out test directories
|
||||
files = [f for f in files if not any(part.lower().startswith('test') for part in f.parts)]
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def find_modules_base_path(root_path: Path) -> Optional[str]:
|
||||
"""
|
||||
Find modules base path by looking for existing modules.
|
||||
Expected pattern: **/modules/*/Program.cs
|
||||
"""
|
||||
module_programs = find_files_recursive(root_path, "modules/*/Program.cs")
|
||||
|
||||
if module_programs:
|
||||
# Extract the modules directory path
|
||||
# E.g., /path/to/src/IoTEdgeModules/modules/foo/Program.cs -> src/IoTEdgeModules/modules
|
||||
first_match = module_programs[0]
|
||||
modules_dir = first_match.parent.parent # Go up from /foo/Program.cs to /modules
|
||||
relative_path = modules_dir.relative_to(root_path)
|
||||
return str(relative_path).replace('\\', '/')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_contracts_project(root_path: Path) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Find contracts project by looking for *Modules.Contracts*.csproj or *Contracts*.csproj
|
||||
Returns dict with path and name, or None if not found.
|
||||
"""
|
||||
# Try specific pattern first
|
||||
contracts_projects = find_files_recursive(root_path, "*Modules.Contracts*.csproj")
|
||||
|
||||
# Fallback to generic contracts pattern
|
||||
if not contracts_projects:
|
||||
contracts_projects = find_files_recursive(root_path, "*Contracts*.csproj")
|
||||
|
||||
if contracts_projects:
|
||||
first_match = contracts_projects[0]
|
||||
project_dir = first_match.parent
|
||||
project_name = first_match.stem # Filename without .csproj
|
||||
relative_path = project_dir.relative_to(root_path)
|
||||
|
||||
return {
|
||||
"path": str(relative_path).replace('\\', '/'),
|
||||
"name": project_name
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_deployment_manifests(root_path: Path) -> list[str]:
|
||||
"""
|
||||
Find all deployment manifest files (*.deployment.manifest.json).
|
||||
Returns list of relative paths.
|
||||
"""
|
||||
manifests = find_files_recursive(root_path, "*.deployment.manifest.json")
|
||||
return [str(m.relative_to(root_path)).replace('\\', '/') for m in manifests]
|
||||
|
||||
|
||||
def extract_namespace_from_csharp(root_path: Path, contracts_path: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Extract project namespace by:
|
||||
1. Reading a C# file from contracts project (if available)
|
||||
2. Finding pattern: namespace <Something>.Modules.Contracts.<ModuleName>
|
||||
3. Extracting base: <Something>
|
||||
"""
|
||||
cs_files = []
|
||||
|
||||
# Try contracts project first
|
||||
if contracts_path:
|
||||
contracts_full_path = root_path / contracts_path
|
||||
cs_files = list(contracts_full_path.rglob("*.cs"))
|
||||
|
||||
# Fallback: scan any .cs file in project
|
||||
if not cs_files:
|
||||
cs_files = list(root_path.rglob("modules/*/*.cs"))[:10] # Sample first 10
|
||||
|
||||
# Pattern to match: namespace <Something>.Modules.Contracts.<ModuleName> or similar
|
||||
namespace_pattern = re.compile(r'namespace\s+([A-Za-z0-9.]+?)\.Modules(?:\.Contracts)?(?:\.[A-Za-z0-9]+)?;')
|
||||
|
||||
for cs_file in cs_files:
|
||||
try:
|
||||
content = cs_file.read_text(encoding='utf-8')
|
||||
match = namespace_pattern.search(content)
|
||||
if match:
|
||||
# Extract the base namespace before .Modules
|
||||
return match.group(1)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_container_registry(root_path: Path, manifest_paths: list[str]) -> Optional[str]:
|
||||
"""
|
||||
Extract container registry URL from existing module.json or deployment manifests.
|
||||
Looks for "repository": "registry.azurecr.io/modulename" pattern.
|
||||
"""
|
||||
registry_pattern = re.compile(r'"repository":\s*"([^/"]+)/[^"]+"')
|
||||
|
||||
# First try module.json files (more reliable)
|
||||
module_jsons = find_files_recursive(root_path, "modules/*/module.json")
|
||||
for module_json in module_jsons[:5]: # Check first 5
|
||||
try:
|
||||
content = module_json.read_text(encoding='utf-8')
|
||||
match = registry_pattern.search(content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Fallback to deployment manifests
|
||||
for manifest_path in manifest_paths:
|
||||
try:
|
||||
full_path = root_path / manifest_path
|
||||
content = full_path.read_text(encoding='utf-8')
|
||||
match = registry_pattern.search(content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_nuget_feed_url(root_path: Path, modules_base_path: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Extract NuGet feed URL from existing Dockerfiles.
|
||||
Looks for VSS_NUGET_EXTERNAL_FEED_ENDPOINTS pattern.
|
||||
"""
|
||||
dockerfiles = find_files_recursive(root_path, "modules/*/Dockerfile*")
|
||||
|
||||
# Match both regular and escaped quotes: "endpoint":"url" or \"endpoint\":\"url\"
|
||||
nuget_pattern = re.compile(r'endpoint\\?":\\s*\\?"(https://[^"\\]+/nuget/v3/index\.json)\\?"')
|
||||
|
||||
for dockerfile in dockerfiles[:10]: # Check first 10
|
||||
try:
|
||||
content = dockerfile.read_text(encoding='utf-8')
|
||||
match = nuget_pattern.search(content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def load_saved_config(root_path: Path) -> Optional[Dict[str, Any]]:
|
||||
"""Load saved project configuration if it exists."""
|
||||
config_path = root_path / ".claude" / ".iot-edge-module-config.json"
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
return json.loads(config_path.read_text(encoding='utf-8'))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save_project_config(root_path: Path, config: Dict[str, Any]) -> bool:
|
||||
"""Save project configuration for future runs."""
|
||||
config_path = root_path / ".claude" / ".iot-edge-module-config.json"
|
||||
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(json.dumps(config, indent=2), encoding='utf-8')
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save config: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def detect_project_structure(root_path: Path, force_detect: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Detect project structure by scanning for patterns.
|
||||
|
||||
Args:
|
||||
root_path: Root directory of the project
|
||||
force_detect: If True, ignore saved config and re-detect
|
||||
|
||||
Returns:
|
||||
Dictionary with detected configuration
|
||||
"""
|
||||
# Try to load saved config first
|
||||
if not force_detect:
|
||||
saved_config = load_saved_config(root_path)
|
||||
if saved_config:
|
||||
saved_config["config_source"] = "saved"
|
||||
return saved_config
|
||||
|
||||
# Perform detection
|
||||
modules_base_path = find_modules_base_path(root_path)
|
||||
contracts_project = find_contracts_project(root_path)
|
||||
manifest_paths = find_deployment_manifests(root_path)
|
||||
|
||||
contracts_path = contracts_project["path"] if contracts_project else None
|
||||
project_namespace = extract_namespace_from_csharp(root_path, contracts_path)
|
||||
container_registry = extract_container_registry(root_path, manifest_paths)
|
||||
nuget_feed_url = extract_nuget_feed_url(root_path, modules_base_path)
|
||||
|
||||
config = {
|
||||
"config_source": "detected",
|
||||
"modules_base_path": modules_base_path,
|
||||
"contracts_project_path": contracts_path,
|
||||
"contracts_project_name": contracts_project["name"] if contracts_project else None,
|
||||
"manifests_found": manifest_paths,
|
||||
"manifests_base_path": os.path.dirname(manifest_paths[0]) if manifest_paths else None,
|
||||
"project_namespace": project_namespace,
|
||||
"container_registry": container_registry,
|
||||
"nuget_feed_url": nuget_feed_url,
|
||||
"has_contracts_project": contracts_project is not None,
|
||||
"has_nuget_feed": nuget_feed_url is not None
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Detect IoT Edge project structure"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Root directory of the project (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force re-detection, ignore saved config"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save",
|
||||
action="store_true",
|
||||
help="Save detected configuration for future runs"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
root_path = Path(args.root).resolve()
|
||||
|
||||
if not root_path.exists():
|
||||
print(json.dumps({"error": f"Path does not exist: {root_path}"}))
|
||||
sys.exit(1)
|
||||
|
||||
# Detect structure
|
||||
config = detect_project_structure(root_path, force_detect=args.force)
|
||||
|
||||
# Save if requested
|
||||
if args.save and config["config_source"] == "detected":
|
||||
if save_project_config(root_path, config):
|
||||
config["config_saved"] = True
|
||||
|
||||
# Output as JSON
|
||||
print(json.dumps(config, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
286
skills/iot-edge-module/scripts/manage_solution.py
Normal file
286
skills/iot-edge-module/scripts/manage_solution.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Solution File Manager for IoT Edge Module Scaffolding
|
||||
|
||||
Manages adding newly created modules to solution files.
|
||||
Supports:
|
||||
- .slnx (XML-based, modern format) - auto-add capability
|
||||
- .sln (legacy format with GUIDs) - manual instructions only
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_solution_file(root_path: Path) -> dict:
|
||||
"""
|
||||
Find solution file in project root.
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- type: "slnx", "sln", or "none"
|
||||
- path: Path to solution file or None
|
||||
- name: Solution file name or None
|
||||
"""
|
||||
# Always exclude test directories
|
||||
def is_test_dir(path: Path) -> bool:
|
||||
return any(part.lower().startswith('test') for part in path.parts)
|
||||
|
||||
# Look for .slnx first (modern format)
|
||||
slnx_files = [f for f in root_path.rglob("*.slnx") if not is_test_dir(f)]
|
||||
if slnx_files:
|
||||
# Use the first one found, preferring root directory
|
||||
slnx_files.sort(key=lambda p: (len(p.parts), p))
|
||||
return {
|
||||
"type": "slnx",
|
||||
"path": slnx_files[0],
|
||||
"name": slnx_files[0].name
|
||||
}
|
||||
|
||||
# Fall back to .sln (legacy format)
|
||||
sln_files = [f for f in root_path.rglob("*.sln") if not is_test_dir(f)]
|
||||
if sln_files:
|
||||
sln_files.sort(key=lambda p: (len(p.parts), p))
|
||||
return {
|
||||
"type": "sln",
|
||||
"path": sln_files[0],
|
||||
"name": sln_files[0].name
|
||||
}
|
||||
|
||||
return {
|
||||
"type": "none",
|
||||
"path": None,
|
||||
"name": None
|
||||
}
|
||||
|
||||
|
||||
def add_module_to_slnx(slnx_path: Path, module_csproj_path: str) -> dict:
|
||||
"""
|
||||
Add module to .slnx solution file.
|
||||
|
||||
Args:
|
||||
slnx_path: Path to .slnx file
|
||||
module_csproj_path: Relative path to module .csproj (e.g., "src/IoTEdgeModules/modules/mymodule/MyModule.csproj")
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success: bool
|
||||
- message: str
|
||||
- action: "added", "already_exists", or "error"
|
||||
"""
|
||||
try:
|
||||
# Parse XML
|
||||
tree = ET.parse(slnx_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Find or create /modules/ folder
|
||||
modules_folder = None
|
||||
for folder in root.findall("Folder"):
|
||||
if folder.get("Name") == "/modules/":
|
||||
modules_folder = folder
|
||||
break
|
||||
|
||||
if modules_folder is None:
|
||||
# Create /modules/ folder if it doesn't exist
|
||||
modules_folder = ET.SubElement(root, "Folder")
|
||||
modules_folder.set("Name", "/modules/")
|
||||
|
||||
# Check if module already exists
|
||||
for project in modules_folder.findall("Project"):
|
||||
if project.get("Path") == module_csproj_path:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Module already exists in solution: {module_csproj_path}",
|
||||
"action": "already_exists"
|
||||
}
|
||||
|
||||
# Get all existing project paths for alphabetical insertion
|
||||
existing_projects = [(p.get("Path"), p) for p in modules_folder.findall("Project")]
|
||||
existing_paths = [path for path, _ in existing_projects]
|
||||
|
||||
# Find insertion point (alphabetical order)
|
||||
insertion_index = 0
|
||||
for i, existing_path in enumerate(existing_paths):
|
||||
if module_csproj_path.lower() < existing_path.lower():
|
||||
insertion_index = i
|
||||
break
|
||||
insertion_index = i + 1
|
||||
|
||||
# Create new project element
|
||||
new_project = ET.Element("Project")
|
||||
new_project.set("Path", module_csproj_path)
|
||||
|
||||
# Insert at the calculated position
|
||||
modules_folder.insert(insertion_index, new_project)
|
||||
|
||||
# Write back to file with proper formatting
|
||||
# Add indentation
|
||||
indent_xml(root)
|
||||
tree.write(slnx_path, encoding="utf-8", xml_declaration=False)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added module to solution at position {insertion_index + 1} of {len(existing_paths) + 1}",
|
||||
"action": "added",
|
||||
"insertion_index": insertion_index,
|
||||
"total_modules": len(existing_paths) + 1
|
||||
}
|
||||
|
||||
except ET.ParseError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Failed to parse .slnx file: {e}",
|
||||
"action": "error"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error adding module to .slnx: {e}",
|
||||
"action": "error"
|
||||
}
|
||||
|
||||
|
||||
def indent_xml(elem, level=0):
|
||||
"""
|
||||
Recursively add indentation to an XML element and its children for pretty printing.
|
||||
|
||||
Args:
|
||||
elem: xml.etree.ElementTree.Element
|
||||
The XML element to indent.
|
||||
level: int, optional
|
||||
The current indentation level (default is 0).
|
||||
|
||||
Returns:
|
||||
None. The function modifies the XML element in place.
|
||||
"""
|
||||
indent = "\n" + " " * level
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = indent + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = indent
|
||||
for child in elem:
|
||||
indent_xml(child, level + 1)
|
||||
if not child.tail or not child.tail.strip():
|
||||
child.tail = indent
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = indent
|
||||
|
||||
|
||||
def get_sln_manual_instructions(module_csproj_path: str, module_name: str) -> str:
|
||||
"""
|
||||
Generate manual instructions for adding module to .sln file.
|
||||
|
||||
.sln files require GUIDs which are complex to generate correctly,
|
||||
so we provide manual instructions instead.
|
||||
"""
|
||||
instructions = f"""
|
||||
Manual instructions for adding module to .sln file:
|
||||
|
||||
.sln files require project GUIDs which must be generated. The easiest way is to use Visual Studio or the dotnet CLI:
|
||||
|
||||
Option 1: Using dotnet CLI (recommended):
|
||||
dotnet sln add "{module_csproj_path}"
|
||||
|
||||
Option 2: Using Visual Studio:
|
||||
1. Open the solution in Visual Studio
|
||||
2. Right-click on the solution in Solution Explorer
|
||||
3. Select "Add" → "Existing Project"
|
||||
4. Navigate to and select: {module_csproj_path}
|
||||
|
||||
Option 3: Manual editing (advanced):
|
||||
1. Open the .sln file in a text editor
|
||||
2. Generate a new GUID (use online generator or PowerShell: [guid]::NewGuid())
|
||||
3. Add project entry:
|
||||
|
||||
Project("{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}") = "{module_name}", "{module_csproj_path}", "{{YOUR-NEW-GUID}}"
|
||||
EndProject
|
||||
|
||||
4. Add to solution configuration platforms section (match existing patterns)
|
||||
|
||||
Note: The dotnet CLI approach is recommended as it handles all GUID generation automatically.
|
||||
"""
|
||||
return instructions
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Manage solution file updates for IoT Edge modules"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Root directory to search for solution file (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--detect",
|
||||
action="store_true",
|
||||
help="Detect solution file type and location"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--add-module",
|
||||
type=str,
|
||||
help="Relative path to module .csproj to add to solution"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--module-name",
|
||||
type=str,
|
||||
help="Module name (for manual instructions)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
root_path = Path(args.root).resolve()
|
||||
|
||||
# Detect solution file
|
||||
solution_info = find_solution_file(root_path)
|
||||
|
||||
if args.detect:
|
||||
# Just output detection results
|
||||
print(json.dumps(solution_info, indent=2, default=str))
|
||||
return 0
|
||||
|
||||
if args.add_module:
|
||||
if solution_info["type"] == "none":
|
||||
result = {
|
||||
"success": False,
|
||||
"message": "No solution file found",
|
||||
"action": "error"
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
return 1
|
||||
|
||||
if solution_info["type"] == "slnx":
|
||||
# Auto-add to .slnx
|
||||
result = add_module_to_slnx(
|
||||
solution_info["path"],
|
||||
args.add_module
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result["success"] else 1
|
||||
|
||||
elif solution_info["type"] == "sln":
|
||||
# Provide manual instructions for .sln
|
||||
module_name = args.module_name or Path(args.add_module).stem
|
||||
instructions = get_sln_manual_instructions(args.add_module, module_name)
|
||||
result = {
|
||||
"success": True,
|
||||
"message": "Manual instructions generated for .sln file",
|
||||
"action": "manual_instructions",
|
||||
"instructions": instructions,
|
||||
"solution_path": str(solution_info["path"])
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0
|
||||
|
||||
# No action specified
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
138
skills/iot-edge-module/scripts/scan_manifests.py
Normal file
138
skills/iot-edge-module/scripts/scan_manifests.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scan for IoT Edge deployment manifest files.
|
||||
|
||||
Finds all *.deployment.manifest.json files in the project and returns
|
||||
metadata about each manifest.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def extract_manifest_metadata(manifest_path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract metadata from a deployment manifest file.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to the manifest file
|
||||
|
||||
Returns:
|
||||
Dictionary with manifest metadata
|
||||
"""
|
||||
try:
|
||||
content = manifest_path.read_text(encoding='utf-8')
|
||||
manifest_data = json.loads(content)
|
||||
|
||||
# Extract module count from $edgeAgent
|
||||
modules_count = 0
|
||||
module_names = []
|
||||
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
|
||||
|
||||
# Navigate the nested JSON structure correctly
|
||||
for key in edge_agent.keys():
|
||||
if key.startswith("properties.desired.modules."):
|
||||
modules_count += 1
|
||||
# Extract module name from key like "properties.desired.modules.modulename"
|
||||
module_name = key.replace("properties.desired.modules.", "")
|
||||
module_names.append(module_name)
|
||||
|
||||
# Extract route count from $edgeHub
|
||||
routes_count = 0
|
||||
edge_hub = manifest_data.get("modulesContent", {}).get("$edgeHub", {})
|
||||
for key in edge_hub.keys():
|
||||
if key.startswith("properties.desired.routes."):
|
||||
routes_count += 1
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"modules_count": modules_count,
|
||||
"module_names": module_names,
|
||||
"routes_count": routes_count
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def scan_manifests(root_path: Path) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Scan for all deployment manifest files, excluding test directories.
|
||||
|
||||
Args:
|
||||
root_path: Root directory of the project
|
||||
|
||||
Returns:
|
||||
List of dictionaries with manifest information
|
||||
"""
|
||||
manifests = list(root_path.rglob("*.deployment.manifest.json"))
|
||||
|
||||
# Always exclude test directories
|
||||
manifests = [m for m in manifests if not any(part.lower().startswith('test') for part in m.parts)]
|
||||
|
||||
results = []
|
||||
for manifest_path in manifests:
|
||||
relative_path = manifest_path.relative_to(root_path)
|
||||
metadata = extract_manifest_metadata(manifest_path)
|
||||
|
||||
result = {
|
||||
"path": str(relative_path).replace('\\', '/'),
|
||||
"name": manifest_path.name,
|
||||
"basename": manifest_path.stem.replace('.deployment.manifest', ''),
|
||||
"absolute_path": str(manifest_path),
|
||||
**metadata
|
||||
}
|
||||
|
||||
results.append(result)
|
||||
|
||||
# Sort by path for consistent ordering
|
||||
results.sort(key=lambda x: x["path"])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Scan for IoT Edge deployment manifest files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Root directory of the project (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Include detailed metadata for each manifest"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
root_path = Path(args.root).resolve()
|
||||
|
||||
if not root_path.exists():
|
||||
print(json.dumps({"error": f"Path does not exist: {root_path}"}))
|
||||
sys.exit(1)
|
||||
|
||||
# Scan for manifests
|
||||
manifests = scan_manifests(root_path)
|
||||
|
||||
# Output as JSON
|
||||
output = {
|
||||
"manifests_found": len(manifests),
|
||||
"manifests": manifests
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
303
skills/iot-edge-module/scripts/update_deployment_manifest.py
Normal file
303
skills/iot-edge-module/scripts/update_deployment_manifest.py
Normal file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Update IoT Edge deployment manifest with a new module.
|
||||
|
||||
This script:
|
||||
1. Parses the deployment manifest JSON
|
||||
2. Finds the highest startupOrder in $edgeAgent.modules
|
||||
3. Inserts a new module definition with calculated startupOrder
|
||||
4. Adds a default route to $edgeHub
|
||||
5. Validates and saves the updated JSON
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
def find_highest_startup_order(manifest_data: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Find the highest startupOrder value in existing modules.
|
||||
|
||||
Args:
|
||||
manifest_data: Parsed manifest JSON
|
||||
|
||||
Returns:
|
||||
Highest startupOrder value, or 0 if no modules found
|
||||
"""
|
||||
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
|
||||
|
||||
# Find all keys that start with "properties.desired.modules."
|
||||
modules = {}
|
||||
for key, value in edge_agent.items():
|
||||
if key.startswith("properties.desired.modules."):
|
||||
modules[key] = value
|
||||
|
||||
highest_order = 0
|
||||
for module_config in modules.values():
|
||||
startup_order = module_config.get("startupOrder", 0)
|
||||
highest_order = max(highest_order, startup_order)
|
||||
|
||||
return highest_order
|
||||
|
||||
|
||||
def create_module_definition(
|
||||
module_name: str,
|
||||
container_registry: str,
|
||||
with_volume: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a standard module definition for $edgeAgent.
|
||||
|
||||
Args:
|
||||
module_name: Lowercase module name
|
||||
container_registry: Container registry URL
|
||||
with_volume: Whether to include volume mount
|
||||
|
||||
Returns:
|
||||
Module definition dictionary
|
||||
"""
|
||||
create_options = {
|
||||
"HostConfig": {
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {
|
||||
"max-size": "10m",
|
||||
"max-file": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add volume mount if requested
|
||||
if with_volume:
|
||||
create_options["HostConfig"]["Mounts"] = [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Target": "/app/data/",
|
||||
"Source": module_name
|
||||
}
|
||||
]
|
||||
|
||||
module_def = {
|
||||
"version": "1.0",
|
||||
"type": "docker",
|
||||
"status": "running",
|
||||
"restartPolicy": "always",
|
||||
"startupOrder": 1,
|
||||
"settings": {
|
||||
"image": f"${{MODULES.{module_name}}}",
|
||||
"createOptions": create_options
|
||||
}
|
||||
}
|
||||
|
||||
return module_def
|
||||
|
||||
|
||||
def create_default_route(module_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a default route for the module to IoT Hub.
|
||||
|
||||
Args:
|
||||
module_name: Lowercase module name
|
||||
|
||||
Returns:
|
||||
Route definition dictionary
|
||||
"""
|
||||
return {
|
||||
"route": f"FROM /messages/modules/{module_name}/outputs/* INTO $upstream",
|
||||
"priority": 0,
|
||||
"timeToLiveSecs": 86400
|
||||
}
|
||||
|
||||
|
||||
def module_exists(manifest_data: Dict[str, Any], module_name: str) -> bool:
|
||||
"""
|
||||
Check if a module already exists in the manifest.
|
||||
|
||||
Args:
|
||||
manifest_data: Parsed manifest JSON
|
||||
module_name: Module name to check
|
||||
|
||||
Returns:
|
||||
True if module exists, False otherwise
|
||||
"""
|
||||
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
|
||||
module_key = f"properties.desired.modules.{module_name}"
|
||||
return module_key in edge_agent
|
||||
|
||||
|
||||
def add_module_to_manifest(
|
||||
manifest_data: Dict[str, Any],
|
||||
module_name: str,
|
||||
container_registry: str,
|
||||
with_volume: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Add a new module to the deployment manifest.
|
||||
|
||||
Args:
|
||||
manifest_data: Parsed manifest JSON
|
||||
module_name: Lowercase module name
|
||||
container_registry: Container registry URL
|
||||
with_volume: Whether to include volume mount
|
||||
|
||||
Returns:
|
||||
Updated manifest data with module added
|
||||
|
||||
Raises:
|
||||
ValueError: If module already exists
|
||||
"""
|
||||
# Check if module already exists
|
||||
if module_exists(manifest_data, module_name):
|
||||
raise ValueError(f"Module '{module_name}' already exists in manifest")
|
||||
|
||||
# Create module definition with startupOrder = 1
|
||||
module_def = create_module_definition(
|
||||
module_name,
|
||||
container_registry,
|
||||
with_volume
|
||||
)
|
||||
|
||||
# Add to $edgeAgent using dotted key (Azure IoT Edge format)
|
||||
if "modulesContent" not in manifest_data:
|
||||
manifest_data["modulesContent"] = {}
|
||||
if "$edgeAgent" not in manifest_data["modulesContent"]:
|
||||
manifest_data["modulesContent"]["$edgeAgent"] = {}
|
||||
|
||||
module_key = f"properties.desired.modules.{module_name}"
|
||||
manifest_data["modulesContent"]["$edgeAgent"][module_key] = module_def
|
||||
|
||||
# Create and add route to $edgeHub using dotted key (Azure IoT Edge format)
|
||||
route_name = f"{module_name}ToIoTHub"
|
||||
route_def = create_default_route(module_name)
|
||||
|
||||
if "$edgeHub" not in manifest_data["modulesContent"]:
|
||||
manifest_data["modulesContent"]["$edgeHub"] = {}
|
||||
|
||||
route_key = f"properties.desired.routes.{route_name}"
|
||||
manifest_data["modulesContent"]["$edgeHub"][route_key] = route_def
|
||||
|
||||
return manifest_data
|
||||
|
||||
|
||||
def update_manifest_file(
|
||||
manifest_path: Path,
|
||||
module_name: str,
|
||||
container_registry: str,
|
||||
with_volume: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update a deployment manifest file with a new module.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
module_name: Lowercase module name
|
||||
container_registry: Container registry URL
|
||||
with_volume: Whether to include volume mount
|
||||
|
||||
Returns:
|
||||
Dictionary with operation result
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If manifest file doesn't exist
|
||||
json.JSONDecodeError: If manifest is invalid JSON
|
||||
ValueError: If module already exists
|
||||
"""
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"Manifest file not found: {manifest_path}")
|
||||
|
||||
# Read and parse manifest
|
||||
content = manifest_path.read_text(encoding='utf-8')
|
||||
manifest_data = json.loads(content)
|
||||
|
||||
# Store original for comparison
|
||||
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
|
||||
original_module_count = len([k for k in edge_agent.keys() if k.startswith("properties.desired.modules.")])
|
||||
|
||||
# Add module
|
||||
updated_manifest = add_module_to_manifest(
|
||||
manifest_data,
|
||||
module_name,
|
||||
container_registry,
|
||||
with_volume
|
||||
)
|
||||
|
||||
# Write back to file
|
||||
updated_content = json.dumps(updated_manifest, indent=2)
|
||||
manifest_path.write_text(updated_content, encoding='utf-8')
|
||||
|
||||
edge_agent_updated = updated_manifest.get("modulesContent", {}).get("$edgeAgent", {})
|
||||
new_module_count = len([k for k in edge_agent_updated.keys() if k.startswith("properties.desired.modules.")])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"manifest_path": str(manifest_path),
|
||||
"module_name": module_name,
|
||||
"startup_order": 1,
|
||||
"modules_before": original_module_count,
|
||||
"modules_after": new_module_count,
|
||||
"route_added": f"{module_name}ToIoTHub"
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Update IoT Edge deployment manifest with a new module"
|
||||
)
|
||||
parser.add_argument(
|
||||
"manifest_path",
|
||||
type=str,
|
||||
help="Path to deployment manifest file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"module_name",
|
||||
type=str,
|
||||
help="Lowercase module name"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Container registry URL (e.g., myregistry.azurecr.io)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-volume",
|
||||
action="store_true",
|
||||
help="Don't add volume mount to module"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest_path = Path(args.manifest_path)
|
||||
|
||||
try:
|
||||
result = update_manifest_file(
|
||||
manifest_path,
|
||||
args.module_name,
|
||||
args.registry,
|
||||
with_volume=not args.no_volume
|
||||
)
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(json.dumps({"success": False, "error": f"Invalid JSON: {e}"}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"Unexpected error: {e}"}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user