Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ To use FabricOps in your environment, you'll need:
- Appropriate Fabric workspace permissions
- Project Administrator role (Azure DevOps) or equivalent GitHub permissions

### Fabric CLI Version Requirement
**Fabric CLI version 1.0.0+ is required**

Version 1.0.0 introduced the `-f`/`--force` flag in the `get` command to suppress warnings on sensitivity labels. The release notes state:
> "Added a confirmation prompt in get to acknowledge that exported items do not include sensitivity labels; use -f to skip."

Versions <1.0.0 do not support the `-f` switch and will cause automation scripts to fail.

## Project Structure

```
Expand Down
8 changes: 4 additions & 4 deletions automation/scripts/fabric_feature_maintainance.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def filter_layers_by_branch(layers, branch_name_trimmed):
if fabcli.run_command(f"exists {workspace_name_escaped}.Workspace").replace("*", "").strip().lower() == "false":
misc.print_info(f"Creating workspace '{workspace_name}'...", bold=True, end="")
fabcli.run_command(f"create '{workspace_name_escaped}.Workspace' -P capacityname={capacity_name}")
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()
misc.print_success(" ✔", bold=True)

if permissions:
Expand Down Expand Up @@ -105,7 +105,7 @@ def filter_layers_by_branch(layers, branch_name_trimmed):
connection_name = git_settings.get("myGitCredentials").get("connection_name").format(identity_id=identity_id, identity_username=identity_username)

if fabcli.connection_exists(connection_name):
connection_id = fabcli.run_command(f"get .connections/{connection_name}.Connection -q id")
connection_id = fabcli.run_command(f"get .connections/{connection_name}.Connection -q id -f")
git_settings["myGitCredentials"].pop("connection_name", None) # Remove connection name
git_settings["myGitCredentials"]["connectionId"] = connection_id # Add connection id required by Fabric REST API

Expand Down Expand Up @@ -135,7 +135,7 @@ def filter_layers_by_branch(layers, branch_name_trimmed):
misc.print_info(f"{workspace_name} already exist. Feature workspace creation skipped!", bold=True)
if layer_definition.get("git_synchronize_on_commit", False) and not layer_definition.get("git_disconnect_after_initialize", False):
misc.print_info(f" • Synchronizing workspace {workspace_name_escaped} with latest changes from Git...", end="")
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()

git_status = fabcli.get_git_status(workspace_id)

Expand All @@ -158,7 +158,7 @@ def filter_layers_by_branch(layers, branch_name_trimmed):
workspace_name_escaped = workspace_name.replace("/", "\\/")
if layer_definition.get("git_synchronize_on_commit", False) and not layer_definition.get("git_disconnect_after_initialize", False):
misc.print_info(f"Synchronizing workspace {workspace_name_escaped} with latest changes from Git repo...", bold=True, end="")
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()

git_status = fabcli.get_git_status(workspace_id)
if git_status and git_status.get("workspaceHead") == git_status.get("remoteCommitHash"):
Expand Down
2 changes: 1 addition & 1 deletion automation/scripts/fabric_gitsync_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

if layer_definition.get("git_synchronize_on_commit", True) and not layer_definition.get("git_disconnect_after_initialize", False):
misc.print_info(f"Synchronizing workspace {workspace_name_escaped} with latest changes from Git repo...", bold=True, end="")
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()

git_status = fabcli.get_git_status(workspace_id)
if git_status is None:
Expand Down
4 changes: 2 additions & 2 deletions automation/scripts/fabric_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
workspace_name = solution_name.format(layer=layer, environment=environment)
workspace_name_escaped = workspace_name.replace("/", "\\/")

workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()

misc.print_subheader(f"Running release to workspace {workspace_name}!")

Expand Down Expand Up @@ -136,7 +136,7 @@

# Now bind all semantic models to this lakehouse
for semantic_model_name in semantic_models:
semantic_model_id = fabcli.run_command(f"get '/{workspace_name}.Workspace/{semantic_model_name}.SemanticModel' -q id").strip()
semantic_model_id = fabcli.run_command(f"get '/{workspace_name}.Workspace/{semantic_model_name}.SemanticModel' -q id -f").strip()
if not semantic_model_id:
misc.print_warning(f"Semantic model '{semantic_model_name}' not found in workspace {workspace_name}. Skip binding.")
continue
Expand Down
28 changes: 15 additions & 13 deletions automation/scripts/fabric_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

# Authenticate
fabcli.run_command("config set encryption_fallback_enabled true")
fabcli.run_command("config set folder_listing_enabled true")
fabcli.run_command(f"auth login -u {client_id} -p {client_secret} --tenant {tenant_id}")

# Load JSON environment files (main and environment specific) and merge
Expand Down Expand Up @@ -145,7 +146,7 @@
else:
misc.print_warning(f" ⚠ Already exists", bold=True)

workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name_escaped}.Workspace' -q id -f").strip()

# Update layer_definition
layer_definition["workspace_id"] = workspace_id
Expand Down Expand Up @@ -180,19 +181,20 @@
print_item_header = True
for item_type, items in layer_definition.get("items").items():
for item in items:
if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase"}:
if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase", "Warehouse"}:
has_item_connections = True

if not item.get("skip_item_creation", False):
if print_item_header:
print(f" • Creating workspace items:")
print_item_header = False

misc.print_info(f" ◦ {item_type}: {item.get("item_name")}...", end="")
item_folder = f'{item.get("item_folder")}/' if item.get("item_folder") else ""
misc.print_info(f" ◦ {item_type}: {item_folder}{item.get("item_name")}...", end="")

if not fabcli.item_exists(f'{workspace_name_escaped}.Workspace/{item.get("item_name")}.{item_type}'):
fabcli.run_command(f"create '{workspace_name_escaped}.Workspace/{item.get("item_name")}.{item_type}'")
item["item_metadata"] = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{item.get('item_name')}.{item_type}", retry_count=2)
if not fabcli.item_exists(f'{workspace_name_escaped}.Workspace/{item_folder}{item.get("item_name")}.{item_type}'):
fabcli.run_command(f"create '{workspace_name_escaped}.Workspace/{item_folder}{item.get("item_name")}.{item_type}'")
item["item_metadata"] = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{item_folder}{item.get('item_name')}.{item_type}", retry_count=2)

if item_type in {"Lakehouse"}:
# Wait until SQL endpoint provisioning completes; treat missing metadata as still provisioning
Expand All @@ -209,7 +211,7 @@
break
print(".", end="")
time.sleep(2)
item["item_metadata"] = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{item.get('item_name')}.{item_type}")
item["item_metadata"] = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{item_folder}{item.get('item_name')}.{item_type}")

if item["item_metadata"]:
misc.print_success(" ✔")
Expand Down Expand Up @@ -269,7 +271,7 @@
for workspace_name, workspace_identity in workspace_identity_acl.items():
misc.print_info(f" • Assigning workspace identity {workspace_identity} to {workspace_name}...", end="")
try:
identity_id = fabcli.run_command(f"get {workspace_identity}.Workspace -q workspaceIdentity.servicePrincipalId").strip()
identity_id = fabcli.run_command(f"get {workspace_identity}.Workspace -q workspaceIdentity.servicePrincipalId -f").strip()
fabcli.run_command(f"acl set {workspace_name}.Workspace -I {identity_id} -R admin -f")
misc.print_success(" ✔")
except Exception as e:
Expand All @@ -290,20 +292,20 @@
for item_type, items in layer_definition.get("items").items():
for item in items:

if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase"}:
if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase", "Warehouse"}:
connection_name = item.get("connection_name").format(layer=layer, environment=environment)
item["item_metadata"] = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{item.get('item_name')}.{item_type}")
#print(f"/{workspace_name_escaped}.Workspace/{item.get('item_name')}.{item_type}")
misc.print_info(f"\nCreating item connection for {connection_name}...", bold=True, end="")

if item["item_metadata"]:
server = (
item.get("item_metadata").get("properties").get("sqlEndpointProperties").get("connectionString") if item_type == "Lakehouse" else
item.get("item_metadata").get("properties").get("serverFqdn")
item.get("item_metadata").get("properties").get("serverFqdn") if item_type == "SQLDatabase" else
item.get("item_metadata").get("properties").get("connectionString")
)

database = (
item.get("item_name") if item_type == "Lakehouse" else
item.get("item_name") if item_type in ("Lakehouse","Warehouse") else
item.get("item_metadata").get("properties").get("databaseName")
)

Expand Down Expand Up @@ -383,7 +385,7 @@
if layer_definition.get("items"):
for item_type, items in layer_definition.get("items").items():
for item in items:
if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase"}:
if item.get("connection_name") and item_type in {"Lakehouse", "SQLDatabase", "Warehouse"}:
connection_name = item.get("connection_name").format(layer=layer, environment=environment)
misc.print_info(f" • Deleting connection '{connection_name}'... ", bold=False, end="")
if fabcli.run_command(f"exists .connections/{connection_name}.Connection").replace("*", "").strip().lower() == "true":
Expand Down
30 changes: 15 additions & 15 deletions automation/scripts/generate_connection_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@

solution_name = env_definition.get("name")
workspace_name = solution_name.format(layer=layer, environment=environment)
workspace_name_escaped = workspace_name.replace("/", "\\/")
sqldb_item = fabcli.get_item(f"/{workspace_name_escaped}.Workspace/{database}.SQLDatabase")

# Example: You may want to adjust the server/database names per environment
server = sqldb_item.get('properties').get('serverFqdn')
database = sqldb_item.get("properties").get("databaseName")

connection_string = (
f"Server={server};"
f"Database={database};"
f"Authentication=Active Directory Service Principal;"
f"User Id={client_id};"
f"Password={client_secret};"
f"Encrypt=True;"
f"Connection Timeout=60;"
item_type = None
for env_item_type in env_definition.get("layers").get(layer).get("items"):
if env_item_type in ["Warehouse","SQLDatabase"]:
for item in env_definition.get("layers").get(layer).get("items").get(env_item_type):
item_name = item.get("item_name")
if item_name == database:
item_type = env_item_type
break

connection_string = fabcli.generate_connection_string(
workspace_name=workspace_name,
item_type=item_type,
database=database,
client_id=client_id,
client_secret=client_secret
)

with open(args.output_file, "w") as f:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

# Resolve lakehouse connection and SQL endpoint information
workspace_name = solution_name.format(layer=model_layer, environment=dev_environment).replace("/", "\\/")
workspace_id = fabcli.run_command(f"get '{workspace_name}.Workspace' -q id").strip()
workspace_id = fabcli.run_command(f"get '{workspace_name}.Workspace' -q id -f").strip()

semantic_model_id = fabcli.get_item_id(f"/{workspace_name}.Workspace/{semantic_model_name}.SemanticModel", retry_count=2)

Expand Down
36 changes: 32 additions & 4 deletions automation/scripts/modules/fabric_cli_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def run_command(command: str) -> str:
def get_item(item_path: str, retry_count: int = 0):
for attempt in range(retry_count + 1):
try:
cli_response = run_command(f"get {item_path} -q .")
cli_response = run_command(f"get {item_path} -q . -f")
return json.loads(cli_response)
except Exception as e:
if attempt < retry_count:
Expand All @@ -46,7 +46,7 @@ def get_item(item_path: str, retry_count: int = 0):
def get_item_id(item_path: str, retry_count: int = 0):
for attempt in range(retry_count + 1):
try:
cli_response = run_command(f"get {item_path} -q id")
cli_response = run_command(f"get {item_path} -q id -f")
return cli_response.strip()
except Exception as e:
if attempt < retry_count:
Expand All @@ -61,7 +61,7 @@ def get_connection(connection_identifier):
response = run_command(f"api -X get {connection_url} ")
return json.loads(response)
else:
return json.loads(run_command(f"get .connections/{connection_identifier}.Connection -q ."))
return json.loads(run_command(f"get .connections/{connection_identifier}.Connection -q . -f"))


def connection_exists(connection_identifier):
Expand Down Expand Up @@ -207,6 +207,8 @@ def create_fabric_connection(connection_name, connection_type, credential_type,
return None




def add_connection_roleassignment(connection_id, identity_id, identity_type, role):
body = {
"principal": {
Expand Down Expand Up @@ -303,4 +305,30 @@ def poll_operation_status(operation_id):
def takeover_semantic_model(workspace_id, semantic_model_id):
takeover_url = f"groups/{workspace_id}/datasets/{semantic_model_id}/Default.TakeOver"
response = run_command(f"api -A powerbi -X post {takeover_url}")
return json.loads(response)
return json.loads(response)


def generate_connection_string(workspace_name, item_type, database, client_id, client_secret):
print(f"Generating connection string for {item_type} '{database}' in workspace '{workspace_name}'...")
workspace_name_escaped = workspace_name.replace("/", "\\/")
sqldb_item = get_item(f"/{workspace_name_escaped}.Workspace/{database}.{item_type}")
print(sqldb_item)
if item_type == "SQLDatabase":
server = sqldb_item.get('properties').get('serverFqdn')
database = sqldb_item.get("properties").get("databaseName")
elif item_type == "Lakehouse":
server = sqldb_item.get("properties").get('sqlEndpointProperties').get('connectionString')
else:
server = sqldb_item.get('properties').get('connectionString')

connection_string = (
f"Server={server};"
f"Database={database};"
f"Authentication=Active Directory Service Principal;"
f"User Id={client_id};"
f"Password={client_secret};"
f"Encrypt=True;"
f"Connection Timeout=60;"
)

return connection_string
Loading
Loading