Providers/nxOMSAutomationWorker/automationworker/3.x/scripts/onboarding3.py (287 lines of code) (raw):
#!/usr/bin/env python3
# ====================================
# Copyright (c) Microsoft Corporation. All rights reserved.
# ====================================
import configparser
import base64
import datetime
import os
import re
import shutil
import socket
import subprocess
import sys
from optparse import OptionParser
import packagesimportutil
# append worker binary source path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
packagesimportutil.add_all_packages_under_automationworker_to_sys_path()
from worker import configuration3
from worker import httpclientfactory
from worker import linuxutil
from worker import serializerfactory
from worker import util
from worker import diydirs
json = serializerfactory.get_serializer(sys.version_info)
configuration3.clear_config()
configuration3.set_config({configuration3.PROXY_CONFIGURATION_PATH: "/etc/opt/microsoft/omsagent/proxy.conf",
configuration3.WORKER_VERSION: "LinuxDIYRegister",
configuration3.WORKING_DIRECTORY_PATH: "/tmp"})
def get_ip_address():
try:
return socket.gethostbyname(socket.gethostname())
except:
return "127.0.0.1"
DIY_STATE_PATH = diydirs.DIY_STATE_PATH
DIY_WORKING_DIR = diydirs.DIY_WORKING_DIR
def generate_self_signed_certificate(certificate_path, key_path):
"""Creates a self-signed x509 certificate and key pair in the spcified path.
Args:
certificate_path : string, the output path of the certificate
key_path : string, the output path of the key
"""
cmd = ["openssl", "req", "-subj",
"/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/OU=Azure Automation/CN=Hybrid Runbook Worker",
"-new", "-newkey", "rsa:2048", "-days", "365", "-nodes", "-x509", "-keyout", key_path, "-out",
certificate_path]
process, certificate_creation_output, error = linuxutil.popen_communicate(cmd)
error = error.decode() if isinstance(error, bytes) else error
if process.returncode != 0:
raise Exception("Unable to create certificate/key. " + str(error))
print ("Certificate/Key created.")
def sha256_digest(payload):
"""Sha256 digest of the specified payload.
Args:
payload : string, the payload to digest
Returns:
payload_hash : string, the sha256 hash of the payload
"""
cmd = ['echo -n "' + str(json.dumps(json.dumps(payload))) + '" | openssl dgst -sha256 -binary']
process, payload_hash, error = linuxutil.popen_communicate(cmd, shell=True)
error = error.decode() if isinstance(error, bytes) else error
if process.returncode != 0:
raise Exception("Unable to generate payload hash. " + str(error))
return payload_hash
def generate_hmac(str_to_sign, secret):
"""Signs the specified string using the specified secret.
Args:
str_to_sign : string, the string to sign
secret : string, the secret used to sign
Returns:
signed_message : string, the signed str_to_sign
"""
message = str_to_sign.encode('utf-8')
secret = secret.encode('utf-8')
cmd = ['echo -n "' + str(message.decode("utf-8")) + '" | openssl dgst -sha256 -binary -hmac "' + str(secret.decode("utf-8")) + '"']
process, signed_message, error = linuxutil.popen_communicate(cmd, shell=True)
error = error.decode() if isinstance(error, bytes) else error
if process.returncode != 0:
raise Exception("Unable to generate signature. " + str(error))
return signed_message
def create_worker_configuration_file(jrds_uri, automation_account_id, worker_group_name, machine_id,
working_directory_path, state_directory_path, cert_path, key_path,
registration_endpoint, workspace_id, thumbprint, vm_id, is_azure_vm,
gpg_keyring_path, test_mode):
"""Creates the automation hybrid worker configuration3 file.
Args:
jrds_uri : string, the jrds endpoint
automation_account_id : string, the automation account id
worker_group_name : string, the hybrid worker group name
machine_id : string, the machine id
working_directory_path : string, the hybrid worker working directory path
state_directory_path : string, the state directory path
cert_path : string, the the certificate path
key_path : string, the key path
registration_endpoint : string, the registration endpoint
workspace_id : string, the workspace id
thumbprint : string, the certificate thumbprint
is_azure_vm : bool, whether the VM is hosted in Azure
gpg_keyring_path : string, path to the gpg keyring for verifying runbook signatures
test_mode : bool, test mode
Note:
The generated file has to match the latest worker.conf template.
"""
worker_conf_path = os.path.join(state_directory_path, "worker.conf")
config = configparser.ConfigParser()
if os.path.isfile(worker_conf_path):
config.read(worker_conf_path)
conf_file = open(worker_conf_path, 'w')
worker_required_section = configuration3.WORKER_REQUIRED_CONFIG_SECTION
if not config.has_section(worker_required_section):
config.add_section(worker_required_section)
config.set(worker_required_section, configuration3.CERT_PATH, cert_path)
config.set(worker_required_section, configuration3.KEY_PATH, key_path)
config.set(worker_required_section, configuration3.BASE_URI, jrds_uri)
config.set(worker_required_section, configuration3.ACCOUNT_ID, automation_account_id)
config.set(worker_required_section, configuration3.MACHINE_ID, machine_id)
config.set(worker_required_section, configuration3.HYBRID_WORKER_GROUP_NAME, worker_group_name)
config.set(worker_required_section, configuration3.WORKING_DIRECTORY_PATH, working_directory_path)
worker_optional_section = configuration3.WORKER_OPTIONAL_CONFIG_SECTION
if not config.has_section(worker_optional_section):
config.add_section(worker_optional_section)
config.set(worker_optional_section, configuration3.PROXY_CONFIGURATION_PATH,
"/etc/opt/microsoft/omsagent/proxy.conf")
config.set(worker_optional_section, configuration3.STATE_DIRECTORY_PATH, state_directory_path)
if gpg_keyring_path is not None:
config.set(worker_optional_section, configuration3.GPG_PUBLIC_KEYRING_PATH, gpg_keyring_path)
if test_mode is True:
config.set(worker_optional_section, configuration3.BYPASS_CERTIFICATE_VERIFICATION, True)
config.set(worker_optional_section, configuration3.DEBUG_TRACES, True)
metadata_section = configuration3.METADATA_CONFIG_SECTION
if not config.has_section(metadata_section):
config.add_section(metadata_section)
config.set(metadata_section, configuration3.WORKER_TYPE, "diy")
config.set(metadata_section, configuration3.IS_AZURE_VM, str(is_azure_vm))
config.set(metadata_section, configuration3.VM_ID, vm_id)
registration_metadata_section = "registration-metadata"
if not config.has_section(registration_metadata_section):
config.add_section(registration_metadata_section)
config.set(registration_metadata_section, configuration3.REGISTRATION_ENDPOINT, registration_endpoint)
config.set(registration_metadata_section, configuration3.WORKSPACE_ID, workspace_id)
config.set(registration_metadata_section, configuration3.CERTIFICATE_THUMBPRINT, thumbprint)
config.write(conf_file)
conf_file.close()
def get_autoregistered_worker_account_id():
autoregistered_worker_conf_path = "/var/opt/microsoft/omsagent/state/automationworker/worker.conf"
config = configparser.ConfigParser()
if os.path.isfile(autoregistered_worker_conf_path) is False:
print ("No diy worker found. Account validation skipped.")
return None
config.read(autoregistered_worker_conf_path)
account_id = config.get("worker-required", "account_id")
print ("Found existing worker for account id : " + str(account_id))
return account_id
def extract_account_id_from_registration_endpoint(registration_endpoint):
account_id = re.findall("\/accounts\/([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})",
registration_endpoint.lower())
if len(account_id) != 1:
raise Exception("Invalid registration endpoint format.")
return account_id[0]
def invoke_dmidecode():
"""Gets the dmidecode output from the host."""
proc = subprocess.Popen(["su", "-", "root", "-c", "dmidecode"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
dmidecode, error = proc.communicate()
if proc.poll() != 0:
raise Exception("Unable to get dmidecode output : " + str(error))
return dmidecode.decode()
def check_if_conf_file_can_be_written():
worker_conf_path = os.path.join(DIY_STATE_PATH, "worker.conf")
try:
conf_file = open(worker_conf_path, 'w')
conf_file.close()
return True
except Exception as ex:
print("Cannot create worker.conf file because of the following exception : " + str(ex))
return False
def register(options):
environment_prerequisite_validation()
"""Registers the machine against the automation agent service.
Args:
options : dict, the options dictionary
"""
registration_endpoint = options.registration_endpoint
automation_account_key = options.automation_account_key
hybrid_worker_group_name = options.hybrid_worker_group_name
workspace_id = options.workspace_id
# assert workspace exists on the box
state_base_path = "/var/opt/microsoft/omsagent/" + workspace_id + "/state/"
working_directory_base_path = "/var/opt/microsoft/omsagent/" + workspace_id + "/run/"
if os.path.exists(state_base_path) is False or os.path.exists(working_directory_base_path) is False:
raise Exception("Invalid workspace id. Is the specified workspace id registered as the OMSAgent "
"primary worksapce?")
diy_account_id = extract_account_id_from_registration_endpoint(registration_endpoint)
auto_registered_account_id = get_autoregistered_worker_account_id()
if auto_registered_account_id != None and auto_registered_account_id != diy_account_id:
raise Exception("Cannot register, conflicting worker already registered.")
worker_conf_path = os.path.join(DIY_STATE_PATH, "worker.conf")
if os.path.isfile(worker_conf_path) is True:
raise Exception("Unable to register, an existing worker was found. Please deregister any existing worker and "
"try again.")
certificate_path = os.path.join(DIY_STATE_PATH, "worker_diy.crt")
key_path = os.path.join(DIY_STATE_PATH, "worker_diy.key")
machine_id = util.generate_uuid()
# generate state path (certs/conf will be dropped in this path)
if os.path.isdir(DIY_STATE_PATH) is False:
try:
os.makedirs(DIY_STATE_PATH)
except Exception as ex:
print("Registration unsuccessful.")
print("Cannot create directory for certs/conf. Because of the following exception : " + str(ex))
return
generate_self_signed_certificate(certificate_path=certificate_path, key_path=key_path)
issuer, subject, thumbprint = linuxutil.get_cert_info(certificate_path)
# try to extract optional metadata
unknown = "Unknown"
asset_tag = unknown
vm_id = unknown
is_azure_vm = False
try:
dmidecode = invoke_dmidecode()
is_azure_vm = linuxutil.is_azure_vm(dmidecode)
if is_azure_vm:
asset_tag = linuxutil.get_azure_vm_asset_tag()
else:
asset_tag = False
vm_id = linuxutil.get_vm_unique_id_from_dmidecode(sys.byteorder, dmidecode)
except Exception as e:
print (str(e))
pass
# generate payload for registration request
date = datetime.datetime.utcnow().isoformat() + "0-00:00"
payload = {'RunbookWorkerGroup': hybrid_worker_group_name,
"MachineName": socket.gethostname().split(".")[0],
"IpAddress": get_ip_address(),
"Thumbprint": thumbprint,
"Issuer": issuer,
"OperatingSystem": 2,
"SMBIOSAssetTag": asset_tag,
"VirtualMachineId": vm_id,
"Subject": subject}
# the signature generation is based on agent service contract
payload_hash = sha256_digest(payload)
b64encoded_payload_hash = base64.b64encode(payload_hash)
signature = generate_hmac(b64encoded_payload_hash.decode("utf-8") + "\n" + date, automation_account_key)
b64encoded_signature = base64.b64encode(signature)
headers = {'Authorization': 'Shared ' + b64encoded_signature.decode("utf-8"),
'ProtocolVersion': "2.0",
'x-ms-date': date,
"Content-Type": "application/json"}
is_conf_file_writable = check_if_conf_file_can_be_written()
if is_conf_file_writable:
# agent service registration request
http_client_factory = httpclientfactory.HttpClientFactory(certificate_path, key_path, options.test)
http_client = http_client_factory.create_http_client(sys.version_info)
url = registration_endpoint + "/HybridV2(MachineId='" + machine_id + "')"
response = http_client.put(url, headers=headers, data=payload)
if response.status_code != 200:
raise Exception("Failed to register worker. [response_status=" + str(response.status_code) + "]")
response.raw_data = response.raw_data.decode() if isinstance(response.raw_data, bytes) else response.raw_data
registration_response = json.loads(response.raw_data)
account_id = registration_response["AccountId"]
create_worker_configuration_file(registration_response["jobRuntimeDataServiceUri"], account_id,
hybrid_worker_group_name, machine_id, DIY_WORKING_DIR,
DIY_STATE_PATH, certificate_path, key_path, registration_endpoint,
workspace_id, thumbprint, vm_id, is_azure_vm, options.gpg_keyring, options.test)
# generate working directory path
diydirs.create_persistent_diy_dirs()
print ("Registration successful!")
else:
print("Registration cannot be completed because configuration file could not be written. Please check the file permissions for /home/nxautomation folder")
def deregister(options):
environment_prerequisite_validation()
registration_endpoint = options.registration_endpoint
automation_account_key = options.automation_account_key
workspace_id = options.workspace_id
# assert workspace exists on the box
state_base_path = "/var/opt/microsoft/omsagent/" + workspace_id + "/state/"
working_directory_base_path = "/var/opt/microsoft/omsagent/" + workspace_id + "/run/"
if os.path.exists(state_base_path) is False or os.path.exists(working_directory_base_path) is False:
raise Exception("Invalid workspace id. Is the specified workspace id registered as the OMSAgent "
"primary worksapce?")
worker_conf_path = os.path.join(DIY_STATE_PATH, "worker.conf")
certificate_path = os.path.join(DIY_STATE_PATH, "worker_diy.crt")
key_path = os.path.join(DIY_STATE_PATH, "worker_diy.key")
if os.path.exists(worker_conf_path) is False:
raise Exception("Unable to deregister, no worker configuration found on disk.")
if os.path.exists(certificate_path) is False or os.path.exists(key_path) is False:
raise Exception("Unable to deregister, no worker certificate/key found on disk.")
issuer, subject, thumbprint = linuxutil.get_cert_info(certificate_path)
if os.path.exists(worker_conf_path) is False:
raise Exception("Missing worker configuration.")
if os.path.exists(certificate_path) is False:
raise Exception("Missing worker certificate.")
if os.path.exists(key_path) is False:
raise Exception("Missing worker key.")
config = configparser.ConfigParser()
config.read(worker_conf_path)
machine_id = config.get("worker-required", "machine_id")
# generate payload for registration request
date = datetime.datetime.utcnow().isoformat() + "0-00:00"
payload = {"Thumbprint": thumbprint,
"Issuer": issuer,
"Subject": subject}
# the signature generation is based on agent service contract
payload_hash = sha256_digest(payload)
b64encoded_payload_hash = base64.b64encode(payload_hash)
signature = generate_hmac(b64encoded_payload_hash.decode() + '\n' + date, automation_account_key)
b64encoded_signature = base64.b64encode(signature)
headers = {'Authorization': 'Shared ' + b64encoded_signature.decode(),
'ProtocolVersion': "2.0",
'x-ms-date': date,
"Content-Type": "application/json"}
# agent service registration request
http_client_factory = httpclientfactory.HttpClientFactory(certificate_path, key_path, options.test)
http_client = http_client_factory.create_http_client(sys.version_info)
url = registration_endpoint + "/Hybrid(MachineId='" + machine_id + "')"
response = http_client.delete(url, headers=headers, data=payload)
if response.status_code != 200:
raise Exception("Failed to deregister worker. [response_status=" + str(response.status_code) + "]")
if response.status_code == 404:
raise Exception("Unable to deregister. Worker not found.")
print ("Successfuly deregistered worker.")
print ("Cleaning up left over directories.")
try:
shutil.rmtree(DIY_STATE_PATH)
print ("Removed state directory.")
except:
raise Exception("Unable to remove state directory base path.")
try:
shutil.rmtree(DIY_WORKING_DIR)
print ("Removed working directory.")
except:
raise Exception("Unable to remove working directory base path.")
def environment_prerequisite_validation():
"""Validates that basic environment requirements are met for the onboarding operations."""
nxautomation_username = "nxautomation"
if linuxutil.is_existing_user(nxautomation_username) is False:
raise Exception("Missing user : " + nxautomation_username + ". Are you running the lastest OMSAgent version?")
omsagent_username = "omsagent"
if linuxutil.is_existing_user(omsagent_username) is False:
raise Exception("Missing user : " + omsagent_username + ".")
omiusers_group_name = "omiusers"
if linuxutil.is_existing_group(omiusers_group_name) is False:
raise Exception("Missing group : " + omiusers_group_name + ".")
nxautomation_group_name = "nxautomation"
if linuxutil.is_existing_group(omiusers_group_name) is False:
raise Exception("Missing group : " + nxautomation_group_name + ".")