diff --git a/app/image-chooser.py b/app/image-chooser.py index 49959d0..dad8308 100644 --- a/app/image-chooser.py +++ b/app/image-chooser.py @@ -2,19 +2,53 @@ import streamlit as st import pandas as pd import re from azure.identity import DefaultAzureCredential +from azure.mgmt.resource import SubscriptionClient from azure.mgmt.compute import ComputeManagementClient +from jinja2 import Template, Environment, FileSystemLoader +import json +from os import getenv +from dotenv import load_dotenv + +load_dotenv() + +def is_valid(key_name: str): + return key_name in st.session_state and st.session_state[key_name] is not None and len(st.session_state[key_name]) > 0 + +def is_valid_and_not_equal(key_name: str, value: str): + return key_name in st.session_state and st.session_state[key_name] != value def clear_offers(): if 'offers' in st.session_state: del st.session_state.offers if 'selected_offer' in st.session_state: del st.session_state.selected_offer clear_skus() -def clear_selected_sku(): - if 'selected_sku' in st.session_state: del st.session_state.selected_sku - def clear_skus(): if 'skus' in st.session_state: del st.session_state.skus - clear_selected_sku() + if 'selected_sku' in st.session_state: del st.session_state.selected_sku + clear_image_versions() + +def clear_image_versions(): + if 'image_versions' in st.session_state: del st.session_state.image_versions + if 'selected_image_version' in st.session_state: del st.session_state.selected_image_version + +def on_publisher_changed(): + clear_offers() + # st.rerun() + +def on_offer_changed(): + clear_skus() + # st.rerun() + +def on_sku_changed(): + clear_skus() + # st.rerun() + +def on_image_version_changed(): + clear_image_versions() + # st.rerun() + +def version_key(v): + return [int(x) for x in v.split('.')] @st.cache_data def get_publishers(location: str): @@ -32,57 +66,112 @@ def get_skus(location: str, publisher: str, offer: str): def get_image_versions(location: str, publisher: str, offer: str, sku: str): return [version.name for version in compute_client.virtual_machine_images.list(location, publisher, offer, sku)] -subscription_id = "c885a276-c882-483f-b216-42f73715161d" -location = "westeurope" +@st.cache_data +def get_locations(): + subscription_client = SubscriptionClient(credential) + locations = sorted(subscription_client.subscriptions.list_locations(getenv('AZURE_SUBSCRIPTION_ID')), key=lambda l: l.name) + return [ + loc for loc in locations + if loc.metadata.region_type == 'Physical' + ] + +subscription_id = getenv("AZURE_SUBSCRIPTION_ID") +default_location = getenv("AZURE_LOCATION") credential = DefaultAzureCredential() + +locations = get_locations() + compute_client = ComputeManagementClient(credential, subscription_id) st.set_page_config(page_title='Azure Image Chooser', layout='wide') st.title('Azure Image Chooser') -left_col, middle_col, right_col = st.columns(3) +location_cols = st.columns(4) +location = location_cols[0].selectbox('Select Azure Location', + options=[{"name": loc.name, "display_name": loc.display_name} for loc in locations], + index=locations.index(next((l for l in locations if l.name == default_location), default_location)), + format_func=lambda loc: loc['display_name'] + )['name'] + +publisher_col, offer_col, sku_col, version_col = st.columns(4) + +# Publishers if 'publishers' not in st.session_state: st.session_state.publishers = get_publishers(location) clear_offers() -selected_publisher = left_col.selectbox('Select Publisher', options=st.session_state.publishers) +if is_valid('publishers'): + st.session_state.selected_publisher = publisher_col.selectbox('Select Publisher', options=st.session_state.publishers, on_change=on_publisher_changed, index=None) +else: + st.error("No publishers found. Please check your Azure subscription and location.") + st.stop() -if 'selected_publisher' not in st.session_state or selected_publisher != st.session_state.selected_publisher: - st.session_state.selected_publisher = selected_publisher - clear_offers() - -if 'offers' not in st.session_state: +# Offers +if 'offers' not in st.session_state and is_valid('selected_publisher'): st.session_state.offers = get_offers(location, st.session_state.selected_publisher) - clear_skus() -selected_offer = middle_col.selectbox('Select Offer', options=st.session_state.offers) +if is_valid('offers'): + st.session_state.selected_offer = offer_col.selectbox('Select Offer', options=st.session_state.offers, on_change=on_offer_changed, index=None) +elif is_valid('selected_publisher'): + st.info("No offers found for the selected publisher. Please select a different publisher.") + st.stop() +else: + st.stop() -if 'selected_offer' not in st.session_state or selected_offer != st.session_state.selected_offer: - st.session_state.selected_offer = selected_offer - clear_skus() - -if 'skus' not in st.session_state: +# SKUs +if 'skus' not in st.session_state and is_valid('selected_publisher') and is_valid('selected_offer'): st.session_state.skus = get_skus(location, st.session_state.selected_publisher, st.session_state.selected_offer) - clear_selected_sku() -selected_sku = right_col.selectbox('Select SKU', options=st.session_state.skus) +if is_valid('skus'): + st.session_state.selected_sku = sku_col.selectbox('Select SKU', options=st.session_state.skus, on_change=on_sku_changed, index=None) +elif is_valid('selected_offer'): + st.info("No SKUs found for the selected offer. Please select a different offer.") + st.stop() +else: + st.stop() -if 'selected_sku' not in st.session_state or selected_sku != st.session_state.selected_sku: - st.session_state.selected_sku = selected_sku +# Image versions +if 'image_versions' not in st.session_state and is_valid('selected_publisher') and is_valid('selected_offer') and is_valid('selected_sku'): + image_versions = get_image_versions(location, st.session_state.selected_publisher, st.session_state.selected_offer, st.session_state.selected_sku) + # Check if all image version string match the re. + regex = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+$') -def version_key(v): - return [int(x) for x in v.split('.')] + if all(regex.match(version) for version in image_versions): + image_versions = sorted(image_versions, key=version_key) -regex = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+$') + st.session_state.image_versions = image_versions -# Display available image versions -images_versions = get_image_versions(location, st.session_state.selected_publisher, st.session_state.selected_offer, st.session_state.selected_sku) +if is_valid('image_versions'): + st.session_state.selected_image_version = version_col.selectbox('Select Image Version', options=st.session_state.image_versions, index=None) +elif is_valid('selected_sku'): + st.info("No image versions found for the selected SKU. Please select a different SKU.") + st.stop() +else: + st.stop() -# Check if all image version string match the re. -if all(regex.match(version) for version in images_versions): - images_versions = sorted(images_versions, key=version_key) +if is_valid('selected_image_version'): + st.subheader("Usage example") -st.dataframe(images_versions, hide_index=True, column_config={"value": "Image Version"}) + with open("templates.json") as f: + templates = json.load(f) + + def usage_scenario_label(item): + return item['label'] + + layout = st.columns(4) + + selected_file = layout[0].selectbox('Select usage scenario:', options=templates, format_func=usage_scenario_label) + + env = Environment(loader=FileSystemLoader("templates")) + tpl = env.get_template(selected_file['file']) + rendered = tpl.render( + publisher=st.session_state.selected_publisher, + offer=st.session_state.selected_offer, + sku=st.session_state.selected_sku, + version=st.session_state.selected_image_version + ) + + st.code(rendered, language=selected_file['language']) diff --git a/app/requirements.txt b/app/requirements.txt index 347d71d..64839d3 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,5 @@ streamlit azure-identity +azure.mgmt.resource azure-mgmt-compute +python-dotenv diff --git a/app/templates.json b/app/templates.json new file mode 100644 index 0000000..b840e42 --- /dev/null +++ b/app/templates.json @@ -0,0 +1,17 @@ +[ + { + "label": "Terraform VM image reference", + "language": "hcl", + "file": "azurerm_hcl.tpl" + }, + { + "label": "Azure CLI", + "language": "shell", + "file": "shell.tpl" + }, + { + "label": "Azure Resource Manager Template", + "language": "json", + "file": "arm_vm.jsonc" + } +] diff --git a/app/templates/arm_vm.jsonc b/app/templates/arm_vm.jsonc new file mode 100644 index 0000000..04c4c87 --- /dev/null +++ b/app/templates/arm_vm.jsonc @@ -0,0 +1,17 @@ +{ + // This is a partial Azure virtual machine resource template. + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2022-03-01", + "name": "example-vm", + "location": "westeurope", + "properties": { + "storageProfile": { + "imageReference": { + "publisher": "{{ publisher }}", + "offer": "{{ offer }}", + "sku": "{{ sku }}", + "version": "{{ version }}" + } + } + } +} diff --git a/app/templates/azurerm_hcl.tpl b/app/templates/azurerm_hcl.tpl new file mode 100644 index 0000000..7fcab8b --- /dev/null +++ b/app/templates/azurerm_hcl.tpl @@ -0,0 +1,6 @@ +source_image_reference = { + publisher = "{{ publisher }}" + offer = "{{ offer }}" + sku = "{{ sku }}" + version = "{{ version }}" +} diff --git a/app/templates/shell.tpl b/app/templates/shell.tpl new file mode 100644 index 0000000..992c99f --- /dev/null +++ b/app/templates/shell.tpl @@ -0,0 +1 @@ +az create -n MyVM -g MyResourceGroup --image {{ publisher }}:{{ offer }}:{{ sku }}:{{ version }} \ No newline at end of file