Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9cc434b
Merge pull request #239 from CloudBoltSoftware/howdoivideos
mbomb67 Mar 25, 2025
d3ea7a6
Azure AD Import Group and Order Summary XUI Samples
Apr 18, 2025
35f90b5
GCP provisoning Terraform Sample
May 9, 2025
ad54b68
web_client_json
May 9, 2025
3d99902
file path
May 9, 2025
ccc3090
gcp_authentication generated parameter from json
May 9, 2025
83d62ff
json file path fix
May 9, 2025
022087b
nofile
May 9, 2025
fb4ceae
no output
May 9, 2025
6062e29
Added local json file
May 9, 2025
cadb6a9
after code change
May 12, 2025
26da6ae
fix
May 15, 2025
e1ae302
test
May 20, 2025
2b7bb75
Teest
May 20, 2025
8b36b0f
test
May 20, 2025
7858dc7
test
May 20, 2025
409600c
Test
May 20, 2025
31619ae
test
May 20, 2025
da086a3
test
May 20, 2025
acfedfc
test
May 20, 2025
a692feb
Tewts
May 20, 2025
9c250bd
Delete blueprints/gcp_terraform_network_sample directory
oparlak-cmp May 21, 2025
d36b2aa
Added an image to the blueprint
May 23, 2025
2a5c0c8
Merge pull request #244 from CloudBoltSoftware/add_icon_to_sample_bp
ehussm May 23, 2025
a2d34ce
Updated the relevent files to match the json file
May 23, 2025
9e62b35
Merge pull request #245 from CloudBoltSoftware/add_icon_to_sample_bp
ehussm May 23, 2025
b2d6779
Merge pull request #246 from CloudBoltSoftware/howdoivideos
mbomb67 Oct 2, 2025
757fc08
Merge pull request #241 from CloudBoltSoftware/oparlakSamples
mbomb67 Oct 20, 2025
d75dc22
Update AWS AMI ID for AMI and OSBAttribute objects
oparlak-cmp Dec 8, 2025
d8f4021
Merge pull request #247 from CloudBoltSoftware/AWS_AMI_Update_in_place
mbomb67 Dec 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ target/
.idea
.DS_Store
*.swp
.vscode
.vscode
blueprints/gcp_terraform_sample/gcp_creds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
This plug-in updates existing Amazon Machine Image (AMI) records in CloudBolt by
replacing an old AMI ID with a new one and updating the associated template
name across both AmazonMachineImage and OSBuildAttribute models.

Workflow:
1. User selects an OSBuild value.
→ NOTE: The `osbuild` parameter **must act as a control field**, and any
parameters depending on it (such as old_ami_id and region) must be
configured with CloudBolt’s *regenerate_on_change* dependency so that their
option lists refresh dynamically.

2. Based on the selected OSBuild:
- `old_ami_id` list is dynamically generated.
- After an AMI is selected, the available AWS regions are dynamically generated.

3. When executed, the script:
- Locates AMIs that match the selected old AMI ID.
- Updates each AMI's `ami_id` and `templatename`.
- Updates the related OSBuildAttribute template name.

This ensures OSBuild-specific AMIs are updated cleanly and consistently.
"""

from common.methods import set_progress
from externalcontent.models import OSBuild, OSBuildAttribute
from resourcehandlers.aws.models import AmazonMachineImage

import logging

logger = logging.getLogger("AWS")

# Initial parameter values — printed to progress for visibility
old_ami_id = "{{old_ami_id}}"
set_progress(f"Old AMI ID: {old_ami_id}")

region = "{{region}}"
set_progress(f"Region: {region}")

osbuild = "{{osbuild}}"
set_progress(f"OSBuild: {osbuild}")

new_template_name = "{{new_template_name}}"
set_progress(f"New template name: {new_template_name}")

new_ami_id = "{{new_ami_id}}"
set_progress(f"New AMI ID: {new_ami_id}")


def run(job, *args, **kwargs):
"""
Updates all AMIs matching old_ami_id:
- Replaces the AMI ID with new_ami_id
- Updates the template name
- Updates the associated OSBuildAttribute template name
"""
set_progress("Fetching AMIs to update...")
amis = AmazonMachineImage.objects.filter(ami_id=old_ami_id)
set_progress(f"Found {amis.count()} AMIs with old AMI ID {old_ami_id}")

for ami in amis:
set_progress("########################")
set_progress(f"Updating AMI {ami.ami_id} template name to {new_template_name}")

# Update AMI details
ami.ami_id = new_ami_id
ami.templatename = new_template_name
ami.save()
set_progress(f"AMI {new_ami_id}, {ami.ami_id} template name updated")

set_progress("########################")
# Update OSBuildAttribute reference
osba = ami.osbuildattribute_ptr
set_progress(f"Updating OSBuildAttribute template name for AMI {ami.ami_id}")
osba.template_name = new_template_name
osba.save()
set_progress(f"OSBuildAttribute for AMI {ami.ami_id} updated")


def generate_options_for_osbuild(field, **kwargs):
"""
Generates a list of OSBuild options.
NOTE: This parameter should be set as a control field because
dependent parameters rely on its value to regenerate their options.
"""
osbs = OSBuild.objects.filter(
environments__resource_handler__awshandler__isnull=False
).distinct()

options = [(osb.id, osb.name) for osb in osbs]

return {"options": options, "override": True, "sort": True}


def generate_options_for_old_ami_id(
field, control_value=None, control_value_dict=None, **kwargs
):
"""
Generates a list of old AMI IDs based on the selected OSBuild.
This function requires:
- `osbuild` to be the control_value
- The "old_ami_id" parameter to have regenerate_on_change pointing to "osbuild"
"""
options = []

if not control_value:
return {"options": options}

try:
# control_value is OSBuild id or global_id
osbuild = control_value

amis = (
AmazonMachineImage.objects.filter(os_build=osbuild)
.values_list("ami_id", flat=True)
.distinct()
)

options = [(ami, ami) for ami in amis]

except OSBuild.DoesNotExist:
logger.warning(f"OSBuild not found for value: {control_value}")

return {"options": options, "override": True, "sort": True}


def generate_options_for_region(
field, control_value=None, control_value_dict=None, **kwargs
):
"""
Generates AWS regions based on the chosen AMI ID.
Requires:
- `old_ami_id` to be configured as a control_value
- The "region" parameter to regenerate_on_change on "old_ami_id"
"""
options = []

if not control_value:
return {"options": options}

try:
ami_id = control_value
logger.info(f"Control_value: {control_value}")

amis = AmazonMachineImage.objects.filter(ami_id=ami_id)

# Collect distinct regions
regions = {ami.region for ami in amis if ami.region}

options = [(r, r) for r in sorted(regions)]

except OSBuild.DoesNotExist:
logger.warning(f"OSBuild not found for value: {control_value}")

return {"options": options, "override": True, "sort": True}
5 changes: 5 additions & 0 deletions blueprints/gcp_terraform_sample/cmp_variable_maps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"TF_VAR_gcp_role_name": "{{gcp_role_name}}",
"TF_VAR_gcp_user_name": "{{gcp_user_name}}",
"TF_VAR_gcp_project_name": "{{gcp_project_name}}"
}
5 changes: 5 additions & 0 deletions blueprints/gcp_terraform_sample/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "google_project_iam_member" "project" {
project = var.gcp_project_name
role = var.gcp_role_name
member = "user:${var.gcp_user_name}"
}
13 changes: 13 additions & 0 deletions blueprints/gcp_terraform_sample/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">=6.34.0"
}
}
}

provider "google" {
project = var.gcp_project_name
credentials = file("var./var/opt/cloudbolt/proserv/gcp_creds")
}
12 changes: 12 additions & 0 deletions blueprints/gcp_terraform_sample/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
variable "gcp_project_name" {
type = string
description = "GCP Project name"
}
variable "gcp_role_name" {
type = string
description = "Permission name, type roles/REQUESTEDROLE"
}
variable "gcp_user_name" {
type = string
description = "User email address"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"description": "",
"id": "OHK-5a8htglt",
"last_updated": "2024-02-02",
"last_updated": "2025-05-23",
"max_retries": 0,
"maximum_version_required": "",
"minimum_version_required": "8.6",
"name": "Action added in Sample Blueprint From RemoteSource",
"resource_technologies": [],
"script_filename": "cb_plugin_1706898870553853.py",
"script_filename": "cb_plugin_1706898870553853_ZKrRd11.py",
"shared": false,
"target_os_families": [],
"type": "CloudBolt Plug-in"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
UPDATED This is a working sample CloudBolt plug-in for you to start with. The run method is required,
but you can change all the code within it. See the "CloudBolt Plug-ins" section of the docs for
more info and the CloudBolt forge for more examples:
https://github.com/CloudBoltSoftware/cloudbolt-forge/tree/master/actions/cloudbolt_plugins
"""
from common.methods import set_progress


def run(job, *args, **kwargs):
set_progress("This will show up in the job details page in the CB UI, and in the job log")

# Example of how to fetch arguments passed to this plug-in ('server' will be available in
# some cases)
server = kwargs.get('server')
if server:
set_progress("This plug-in is running for server {}".format(server))

set_progress("Dictionary of keyword args passed to this plug-in: {}".format(kwargs.items()))

if True:
return "SUCCESS", "Sample output message", ""
else:
return "FAILURE", "Sample output message", "Sample error message, this is shown in red"
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"any_group_can_deploy": true,
"auto_historical_resources": false,
"blueprint_image": null,
"blueprint_image": "/static/uploads/blueprints/348753.png",
"deployment_items": [
{
"all_environments_enabled": true,
Expand All @@ -15,6 +15,7 @@
"id": "BDI-ur30ucbi",
"name": "server",
"os_build": null,
"rate": null,
"restrict_applications": false,
"show_on_order_form": false,
"tier_type": "server"
Expand All @@ -24,29 +25,32 @@
"continue_on_failure": false,
"deploy_seq": 3,
"description": null,
"enabled": true,
"execute_in_parallel": false,
"id": "BDI-4kz9ngur",
"name": "Action added in Sample Blueprint From RemoteSource",
"rate": null,
"run_on_scale_up": true,
"show_on_order_form": false,
"tier_type": "plugin"
}
],
"description": "",
"favorited": false,
"icon": "348753.png",
"id": "BP-l4qt4nog",
"is_manageable": true,
"is_orderable": true,
"labels": [],
"last_updated": "2024-02-02",
"last_updated": "2025-05-23",
"management_actions": [],
"maximum_version_required": "",
"minimum_version_required": "8.6",
"name": "Sample Blueprint From Remote Source",
"resource_name_template": null,
"resource_type": {
"icon": "",
"id": "RT-at2r6e0j",
"id": "RT-czjq32w0",
"label": "Service",
"lifecycle": "ACTIVE",
"list_view_columns": [],
Expand All @@ -56,4 +60,4 @@
"sequence": 0,
"show_recipient_field_on_order_form": false,
"teardown_items": []
}
}
3 changes: 3 additions & 0 deletions ui_extensions/azure_ad_group_import/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# explicit list of extensions to be included instead of
# the default of .py and .html only
ALLOWED_XUI_EXTENSIONS = [".py", ".html", ".png", ".svg", ".rst"]
41 changes: 41 additions & 0 deletions ui_extensions/azure_ad_group_import/azure_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import requests
from msal import ConfidentialClientApplication
import logging

logger = logging.getLogger(__name__)


def get_access_token(client_id, client_secret, tenant_id):
app = ConfidentialClientApplication(
client_id=client_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential=client_secret
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" not in result:
logger.error("Token acquisition failed: %s", result)
raise Exception(result)
return result["access_token"]


def fetch_all_groups(token):
headers = {"Authorization": f"Bearer {token}"}
url = "https://graph.microsoft.com/v1.0/groups?$top=100"
all_groups = []

while url:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
all_groups.extend(data.get("value", []))
url = data.get("@odata.nextLink")

return all_groups


def get_group_by_id(group_id, token):
headers = {"Authorization": f"Bearer {token}"}
url = f"https://graph.microsoft.com/v1.0/groups/{group_id}"
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
6 changes: 6 additions & 0 deletions ui_extensions/azure_ad_group_import/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from utilities.logger import ThreadLogger

logger = ThreadLogger(__name__)

def run_config(xui_version):
logger.info(f"Azure AD Group s XUI version {xui_version} loaded.")
12 changes: 12 additions & 0 deletions ui_extensions/azure_ad_group_import/templates/admin_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load static %}
{% load helper_tags %}
{% load tab_tags %}
{% block topnav %}Azure AD Groups{% endblock %}
{% block title %}Azure AD Groups{% endblock %}

{% block content %}
<span class="uplink"><a href="{% url 'admin_home' %}">Admin</a></span>
<h1>Azure AD Groups V2</h1>
{% include "azure_ad_group_import/templates/tab-groups.html" %}
{% endblock %}
19 changes: 19 additions & 0 deletions ui_extensions/azure_ad_group_import/templates/group-detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load static %}
{% load helper_tags %}
{% load tab_tags %}
{% block topnav %}Azure AD Group Detail{% endblock %}
{% block title %}Azure AD Group Detail{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Azure AD Group Detail</h2>

{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% else %}
<pre style="background-color: #f8f9fa; padding: 1em; border-radius: 5px; white-space: pre-wrap;">{{ group|safe }}</pre>
{% endif %}

<a href="{% url 'azure_ad_groups_list' %}" class="btn btn-secondary mt-3">← Back to Group List</a>
</div>
{% endblock %}
Loading