Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AzureCLI integration #427

Merged
merged 13 commits into from
Aug 8, 2019
1 change: 1 addition & 0 deletions core/src/epicli/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jsonschema = "*"
python-json-logger = "*"
ansible = "*"
terraform-bin = "*"
azure-cli = "==2.0.67"

[requires]
python_version = "3.7"
1,280 changes: 1,258 additions & 22 deletions core/src/epicli/Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/src/epicli/cli/engine/EpiphanyEngine.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def apply(self):
template_generator.run()

# Run Terraform to create infrastructure
with TerraformRunner(self.cluster_model.specification.name) as tf_runner:
with TerraformRunner(self.cluster_model) as tf_runner:
tf_runner.run()

self.process_configuration_docs()
Expand Down
28 changes: 15 additions & 13 deletions core/src/epicli/cli/engine/TerraformCommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,39 @@ def __init__(self, working_directory=os.path.dirname(__file__)):
self.INIT_COMMAND = "init"
self.working_directory = working_directory

def apply(self, auto_approve=False):
self.run(self, self.APPLY_COMMAND, auto_approve=auto_approve)
def apply(self, auto_approve=False, env=os.environ.copy()):
self.run(self, self.APPLY_COMMAND, auto_approve=auto_approve, env=env)

def destroy(self, auto_approve=False):
self.run(self, self.DESTROY_COMMAND, auto_approve=auto_approve)
def destroy(self, auto_approve=False, env=os.environ.copy()):
self.run(self, self.DESTROY_COMMAND, auto_approve=auto_approve, env=env)

def plan(self):
self.run(self, self.PLAN_COMMAND)
def plan(self, env=os.environ.copy()):
self.run(self, self.PLAN_COMMAND, env=env)

def init(self):
self.run(self, self.INIT_COMMAND)
def init(self, env=os.environ.copy()):
self.run(self, self.INIT_COMMAND, env=env)

@staticmethod
def run(self, command, auto_approve=False):
def run(self, command, env, auto_approve=False):
cmd = ['terraform', command]

if auto_approve:
cmd.append('--auto-approve')

if command == self.APPLY_COMMAND:
cmd.append('-state=' + self.working_directory + '/terraform.tfstate')
cmd.append(f'-state={self.working_directory}/terraform.tfstate')

cmd.append(self.working_directory)

self.logger.info('Running: "' + ' '.join(cmd) + '"')

cmd = ' '.join(cmd)

logpipe = LogPipe(__name__)
with subprocess.Popen(cmd, stdout=logpipe, stderr=logpipe) as sp:
with subprocess.Popen(cmd, stdout=logpipe, stderr=logpipe, env=env, shell=True) as sp:
logpipe.close()

if sp.returncode != 0:
raise Exception('Error running: "' + ' '.join(cmd) + '"')
raise Exception(f'Error running: "{cmd}"')
else:
self.logger.info('Done running "' + ' '.join(cmd) + '"')
self.logger.info(f'Done running "{cmd}"')
36 changes: 31 additions & 5 deletions core/src/epicli/cli/engine/TerraformRunner.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
import os
from cli.engine.TerraformCommand import TerraformCommand
from cli.engine.azure.AzureCommand import AzureCommand
from cli.helpers.Step import Step
from cli.helpers.build_saver import get_terraform_path
from cli.helpers.build_saver import get_terraform_path, save_sp, SP_FILE_NAME
from cli.helpers.data_loader import load_yaml_file


class TerraformRunner(Step):

def __init__(self, cluster_name):
def __init__(self, cluster_model):
super().__init__(__name__)
self.terraform = TerraformCommand(get_terraform_path(cluster_name))
self.cluster_model = cluster_model
self.terraform = TerraformCommand(get_terraform_path(self.cluster_model.specification.name))
self.azure_cli = AzureCommand()

def __enter__(self):
super().__enter__()
return self

def run(self):
self.terraform.init()
self.terraform.apply(auto_approve=True)
new_env = os.environ.copy()
self.terraform.init(env=new_env)

#if the provider is Azure we need to login and setup service principle.
if self.cluster_model.provider == 'azure':
subscription = self.azure_cli.login(self.cluster_model.specification.cloud.subscription_name)

if self.cluster_model.specification.cloud.use_service_principal:
sp_file = os.path.join(get_terraform_path(self.cluster_model.specification.name), SP_FILE_NAME)
if not os.path.exists(sp_file):
self.logger.info('Creating service principle')
sp = self.azure_cli.create_sp(self.cluster_model.specification.cloud.resource_group_name, subscription['id'])
save_sp(sp, self.cluster_model.specification.name)
else:
self.logger.info('Using service principle from file')
sp = load_yaml_file(sp_file)

#Setup environment variables for Terraform when working with Azure.
new_env['ARM_SUBSCRIPTION_ID'] = subscription['id']
new_env['ARM_TENANT_ID'] = sp['tenant']
new_env['ARM_CLIENT_ID'] = sp['appId']
new_env['ARM_CLIENT_SECRET'] = sp['password']

self.terraform.apply(auto_approve=True, env=new_env)
3 changes: 1 addition & 2 deletions core/src/epicli/cli/engine/azure/APIProxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
pass

pass
49 changes: 49 additions & 0 deletions core/src/epicli/cli/engine/azure/AzureCommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
import re
import time
from subprocess import Popen, PIPE
from cli.helpers.Log import LogPipe, Log
from cli.helpers.doc_list_helpers import select_first


class AzureCommand:
def __init__(self):
self.logger = Log(__name__)

def login(self, subscription_name):
all_subscription = self.run(self, 'az login')
subscription = select_first(all_subscription, lambda x: x['name'] == subscription_name)
if subscription is None:
raise Exception(f'User does not have access to subscription: "{subscription_name}"')
self.run(self, f'az account set --subscription {subscription["id"]}')
return subscription

def create_sp(self, app_name, subscription_id):
#TODO: make role configurable?
sp = self.run(self, f'az ad sp create-for-rbac -n "{app_name}" --role="Contributor" --scopes="/subscriptions/{subscription_id}"')
# Sleep for a while. Sometimes the call returns before the rights of the SP are finished creating.
for x in range(0, 20):
self.logger.info(f'Waiting 20 seconds...{x}')
time.sleep(1)
return sp

@staticmethod
def run(self, cmd):
self.logger.info('Running: "' + cmd + '"')

logpipe = LogPipe(__name__)
with Popen(cmd, stdout=PIPE, stderr=logpipe, shell=True) as sp:
logpipe.close()
try:
data = sp.stdout.read().decode('utf-8')
data = re.sub(r'\s+', '', data)
data = re.sub(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]', '', data)
output = json.loads(data)
except:
output = {}

if sp.returncode != 0:
raise Exception(f'Error running: "{cmd}"')
else:
self.logger.info(f'Done running "{cmd}"')
return output
9 changes: 6 additions & 3 deletions core/src/epicli/cli/engine/azure/InfrastructureBuilder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from cli.helpers.Step import Step


class ConfigBuilder(Step):
def __init__(self):
class InfrastructureBuilder(Step):
def __init__(self, docs):
super().__init__(__name__)
self.docs = docs

def run(self):
raise NotImplementedError()
infrastructure = []

return infrastructure

11 changes: 10 additions & 1 deletion core/src/epicli/cli/helpers/build_saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import os
from distutils import dir_util
from cli.helpers.data_loader import load_template_file, types
from cli.helpers.yaml_helpers import dump_all
from cli.helpers.yaml_helpers import dump_all, dump
from cli.helpers.Config import Config

TERRAFORM_OUTPUT_DIR = 'terraform/'
MANIFEST_FILE_NAME = 'manifest.yml'
SP_FILE_NAME = 'sp.yml'
INVENTORY_FILE_NAME = 'inventory'
ANSIBLE_OUTPUT_DIR = 'ansible/'

Expand All @@ -20,6 +21,14 @@ def save_manifest(docs, cluster_name, manifest_name=MANIFEST_FILE_NAME):
return path


def save_sp(service_principle, cluster_name):
terraform_dir = get_terraform_path(cluster_name)
path = os.path.join(terraform_dir, SP_FILE_NAME)
with open(path, 'w') as stream:
dump(service_principle, stream)
return path


def save_inventory(inventory, cluster_model):
cluster_name = cluster_model.specification.name
build_dir = get_build_path(cluster_name)
Expand Down
9 changes: 7 additions & 2 deletions core/src/epicli/cli/helpers/data_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ def load_yaml_obj(file_type, provider, kind):
script_dir = os.path.dirname(__file__)
path_to_file = os.path.join(script_dir, DATA_FOLDER_PATH, provider, file_type, kind+'.yml')
if os.path.isfile(path_to_file):
return load_yaml_file(path_to_file)
with open(path_to_file, 'r') as stream:
return safe_load(stream)
else:
path_to_file = os.path.join(script_dir, DATA_FOLDER_PATH, 'common', file_type, kind + '.yml')
with open(path_to_file, 'r') as stream:
return safe_load(stream)
return load_yaml_file(path_to_file)


def load_yaml_file(path_to_file):
with open(path_to_file, 'r') as stream:
return safe_load(stream)


def load_all_yaml_objs(file_type, provider, kind):
Expand Down
20 changes: 19 additions & 1 deletion core/src/epicli/data/azure/terraform/epiphany-cluster.j2
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# TODO: Fill template
#####################################################
# DO NOT Modify by hand - Manage by Automation
#####################################################
#####################################################
# This file can be used as a base template to build other Terraform files. It attempts to use as much
# Terraform interprolation as possible by creating Terraform variables instead of changing inline
# this approach provides an easier way to do creative looping, fetch IDs of created resources etc.
#####################################################
#####################################################
# {{ specification.name }}
#####################################################

provider "azurerm" {
}

resource "azurerm_resource_group" "rg" {
name = "{{ specification.cloud.resource_group_name }}"
location = "{{ specification.cloud.region }}"
}
2 changes: 2 additions & 0 deletions core/src/epicli/data/common/defaults/epiphany-cluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ specification:
key_path: /root/.ssh/epiphany-operations/id_rsa # YOUR-SSH-KEY-PATH
cloud:
subscription_name: YOUR-SUB-NAME
resource_group_name: YOUR-RESOURCE-GROUP-NAME
seriva marked this conversation as resolved.
Show resolved Hide resolved
vnet_address_pool: 10.1.0.0/20
use_public_ips: False # When not using public IPs you have to provide connectivity via private IPs (VPN)
use_service_principal: False
region: eu-west-2
credentials: # todo change it to get credentials from vault
key: 3124-4124-4124
Expand Down