PK™5HIãkF2F2azuresshconfig.py#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'Yoichi Kawasaki' import sys import os import argparse import simplejson as json try: from msrestazure.azure_active_directory import ServicePrincipalCredentials from azure.mgmt.resource import ResourceManagementClient from azure.mgmt.compute import ComputeManagementClient from azure.mgmt.network import NetworkManagementClient from azure.mgmt.network.models.public_ip_address_dns_settings import PublicIPAddressDnsSettings except ImportError: pass ### Global Defines _AZURE_SSH_CONFIG_VERSION = '0.1.0' _AZURE_SSH_CONFIG_HOME_SITE = 'https://github.com/yokawasa/azure-ssh-config' _DEFAULT_AZURE_SSH_CONFIG_JSON_FILE = '{}/.azure/azuresshconfig.json'.format(os.environ['HOME']) _DEFAULT_SSH_CONFIG_FILE = '{}/.ssh/config'.format(os.environ['HOME']) _DEFAULT_SSH_CONFIG_BLOCK_START_MAKR = "### AZURE-SSH-CONFIG BEGIN ###" _DEFAULT_SSH_CONFIG_BLOCK_END_MARK = "### AZURE-SSH-CONFIG END ###" class SSHConfigBlock: def __init__(self): self._entries = [] self._ssh_config_param_defines = [ 'IdentityFile','User','Port','Protocol','ForwardAgent'] def add_entry(self,entry_name, access_address, params ): entry = { 'Name' : entry_name, 'HostName': access_address } for d in self._ssh_config_param_defines: if exists_in_dict(d, params): entry[d] = params[d] self._entries.append(entry) def to_string(self): buffer = '' for entry in self._entries: buffer += "Host {}\n".format(entry['Name']) buffer += " HostName {}\n".format(entry['HostName']) for d in self._ssh_config_param_defines: if exists_in_dict(d, entry): buffer += " {0} {1}\n".format(d, entry[d]) buffer += "\n" return buffer class SSHConfig: def __init__(self, sshconfig=_DEFAULT_SSH_CONFIG_FILE, block_start_mark=_DEFAULT_SSH_CONFIG_BLOCK_START_MAKR, block_end_mark=_DEFAULT_SSH_CONFIG_BLOCK_END_MARK ): self.sshconfig = sshconfig self._block_start_mark = "{}\n".format(block_start_mark) self._block_end_mark = "{}\n".format(block_end_mark) self._block = '' self._pre_block = '' self._post_block = '' self.__prepare() self.__parse() def __prepare(self): if not os.path.exists(self.sshconfig): # Create an Empty SSH Config file open(self.sshconfig, 'a').close() def __parse(self): try: f = open(self.sshconfig, "r") contents = f.read() start_block_mark_startpos = contents.find(self._block_start_mark) if (start_block_mark_startpos > -1): block_startpos = start_block_mark_startpos + len(self._block_start_mark) block_endpos = contents.find(self._block_end_mark, block_startpos) if (block_endpos > -1): end_block_mark_endpos = block_endpos + len(self._block_end_mark) self._block = contents[block_startpos:block_endpos] self._pre_block = contents[:start_block_mark_startpos] self._post_block = contents[end_block_mark_endpos:] else: print_err("You have start block mark but not end mark!") raise Exception("Unexpected error: invlid block mark: {0}".format(self.sshconfig)) else: self._pre_block = contents except IOError: print_err('Cannot Open %s' % self.sshconfig ) raise else: f.close() def block_exists(self): return True if self._block else False def get_block (self): return self._block def append_block (self,block): self._block = block try: f = open(self.sshconfig,"w") f.write( "{0}{1}{2}\n{3}".format( self._pre_block, self._block_start_mark, block, self._block_end_mark)) except IOError: print_err('Cannot Open %s' % self.sshconfig ) raise else: f.close def update_block (self, block): self._block = block try: f = open(self.sshconfig,"w") f.write( "{0}{1}{2}\n{3}{4}".format( self._pre_block, self._block_start_mark, block, self._block_end_mark, self._post_block)) except IOError: print_err('Cannot Open {}'.format(self.sshconfig) ) raise else: f.close class ClientProfileConfig: def __init__(self,config_file): self.subscription_id = '' self.client_id = '' self.client_scret = '' self.tenant_id = '' self.__open_read_config(config_file) def __open_read_config(self,config_file): try: cf = open(config_file, 'r') o = json.load(cf) self.subscription_id= o['subscription_id'] self.client_id = o['client_id'] self.client_scret = o['client_scret'] self.tenant_id = o['tenant_id'] except IOError: print_err('Cannot Open {}'.format(config_file) ) else: cf.close() def dump(self): print("Azure SSH config client profile dump:\n\tsubscription_id={0}\n" "\tclient_id={1}\n\tclient_scret={2}\n\ttenant_id={3}".format( self.subscription_id, self.client_id, self.client_scret, self.tenant_id)) @staticmethod def generate_template(any_file): try: f = open(any_file,"w") f.write( "{\n" " \"subscription_id\": \"\",\n" " \"client_id\": \"\",\n" " \"client_scret\": \"\",\n" " \"tenant_id\": \"\"\n" "}" ) except IOError: print_err('Cannot Open {}'.format(any_file) ) else: f.close def print_err(s): sys.stderr.write("[ERROR] {}\n".format(s)) def exists_in_list (elem, target_list): return elem in target_list def exists_in_dict (key, target_dict): return True if ( target_dict.has_key(key) and target_dict[key] ) else False def get_resorucegroup_from_vmid (vmid): ids = vmid.split('/') # virutal machine id format: # /subscriptions//resourceGroups//providers//virtualMachines/ if len(ids) != 9: pass return ids[4] def get_network_interface_info (network_client, network_interface_id): ni_info = {} ids = network_interface_id.split('/') # network interface id format: # /subscriptions//resourceGroups//providers//networkInterfaces/ if len(ids) != 9: pass ni = network_client.network_interfaces.get( ids[4], ids[8] ) ipconfigs= ni.ip_configurations ipconfig = ipconfigs[0] # privte ip ni_info['private_ip'] = ipconfig.private_ip_address # get public address public_address_id = ipconfig.public_ip_address.id # public address id format: # /subscriptions//resourceGroups//providers//publicIPAddresses/ pas= public_address_id.split('/') if len(pas) != 9: return ni_info pia = network_client.public_ip_addresses.get(pas[4], pas[8]) if pia.dns_settings: dns_settings = pia.dns_settings if dns_settings.domain_name_label and dns_settings.fqdn: ni_info['fqdn'] = dns_settings.fqdn if pia.ip_address: ni_info['public_ip'] = pia.ip_address return ni_info def main(): parser = argparse.ArgumentParser(description='This program generates SSH config from Azure ARM VM inventry in subscription') parser.add_argument( '--version', action='version', version=_AZURE_SSH_CONFIG_VERSION) parser.add_argument( '--init', action='store_true', help='Create template client profile at $HOME/.azure/azuresshconfig.json only if there is no existing one') parser.add_argument( '--profile', help='Specify azure client profile file to use ($HOME/.azure/azuresshconfig.json by default)') parser.add_argument( '--user', help='SSH username to use for all hosts') parser.add_argument( '--identityfile', help='SSH identity file to use for all hosts') parser.add_argument( '--private', action='store_true', help='Use private IP addresses (Public IP is used by default)') parser.add_argument( '--resourcegroups', help='A comma-separated list of resource group to be considered for ssh-config generation (all resource groups by default)') args = parser.parse_args() if args.init: # check if $HOME/.azure directory exists and create if not exists azure_config_home = '{}/.azure'.format(os.environ['HOME']) if not os.path.exists(azure_config_home): os.makedirs(azure_config_home) # Initialize azure client profile file if not os.path.exists(_DEFAULT_AZURE_SSH_CONFIG_JSON_FILE): ClientProfileConfig.generate_template(_DEFAULT_AZURE_SSH_CONFIG_JSON_FILE) print ("Created template client profile!: {}".format( _DEFAULT_AZURE_SSH_CONFIG_JSON_FILE)) else: print_err("No action has done since client profile file already exist!: {}".format( _DEFAULT_AZURE_SSH_CONFIG_JSON_FILE)) quit() ### Args Validation client_profile_file = args.profile if args.profile else _DEFAULT_AZURE_SSH_CONFIG_JSON_FILE if not os.path.exists(client_profile_file): print_err("Client profile doesn't exist: {0}\n" "For the client profile detail, refer to {1}".format( client_profile_file, _AZURE_SSH_CONFIG_HOME_SITE)) quit() ssh_default_user = args.user if args.user else '' ssh_default_identityfile = args.identityfile if args.identityfile else '' option_private_ip = args.private filter_resource_groups = [] if args.resourcegroups: lower_rg = args.resourcegroups.lower() rslist= lower_rg.split(',') if len(rslist) > 0: filter_resource_groups = rslist ### Load Config cconf = ClientProfileConfig(client_profile_file) ### Get Target virtual machines info List credentials = ServicePrincipalCredentials( cconf.client_id,cconf.client_scret, tenant=cconf.tenant_id) compute_client = ComputeManagementClient( credentials, cconf.subscription_id) network_client = NetworkManagementClient( credentials, cconf.subscription_id) target_vm_list = [] for vm in compute_client.virtual_machines.list_all(): target_vm = {} target_vm['name'] = vm.name vm_rgroup = get_resorucegroup_from_vmid(vm.id) # Filtering by resource group if needed if len(filter_resource_groups) > 0: r = vm_rgroup.lower() if not exists_in_list(r, filter_resource_groups ): continue # skip network_interfaces = vm.network_profile.network_interfaces for ni in network_interfaces: ni_info = get_network_interface_info(network_client, ni.id) if option_private_ip: if exists_in_dict('private_ip',ni_info): target_vm['access_ip'] = ni_info['private_ip'] else: if exists_in_dict('public_ip',ni_info): target_vm['access_ip'] = ni_info['public_ip'] if exists_in_dict('access_ip',target_vm): break # Add only vm that has access_ip if exists_in_dict('access_ip',target_vm): target_vm_list.append(target_vm) ### Generate and append config block to ssh-config file scblock = SSHConfigBlock() for v in target_vm_list: params = {} if ssh_default_user: params['User'] = ssh_default_user if ssh_default_identityfile: params['IdentityFile'] = ssh_default_identityfile scblock.add_entry(v['name'], v['access_ip'],params) ssh_config_block = scblock.to_string() ssh_config = SSHConfig() if ssh_config.block_exists(): ssh_config.update_block(ssh_config_block) else: ssh_config.append_block(ssh_config_block) print "Done! Updated: {}".format(ssh_config.sshconfig) if __name__ == "__main__": main() # # vim:ts=4 et # PK>HI^-Ò .azuresshconfig-0.1.0.dist-info/DESCRIPTION.rstUNKNOWN PK>HI9®"ï66/azuresshconfig-0.1.0.dist-info/entry_points.txt[console_scripts] azuresshconfig=azuresshconfig:main PK>HI)³Æ†WW,azuresshconfig-0.1.0.dist-info/metadata.json{"classifiers": ["Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.7", "Topic :: Utilities"], "extensions": {"python.commands": {"wrap_console": {"azuresshconfig": "azuresshconfig:main"}}, "python.details": {"contacts": [{"email": "yoichi.kawasaki@outlook.com", "name": "Yoichi Kawasaki", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/yokawasa/azure-ssh-config"}}, "python.exports": {"console_scripts": {"azuresshconfig": "azuresshconfig:main"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["azure", "ssh", "config"], "license": "MIT", "metadata_version": "2.0", "name": "azuresshconfig", "platform": "any", "run_requires": [{"requires": ["azure-mgmt-compute (>=0.30.0rc6)", "azure-mgmt-network (>=0.30.0rc6)", "azure-mgmt-resource (>=0.30.0rc6)", "msrestazure", "simplejson"]}], "summary": "Generate SSH config file from Azure ARM VM inventry in subscription", "version": "0.1.0"}PK>HIä å{,azuresshconfig-0.1.0.dist-info/top_level.txtazuresshconfig PK>HIŒ''\\$azuresshconfig-0.1.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK>HI`Ôû('azuresshconfig-0.1.0.dist-info/METADATAMetadata-Version: 2.0 Name: azuresshconfig Version: 0.1.0 Summary: Generate SSH config file from Azure ARM VM inventry in subscription Home-page: https://github.com/yokawasa/azure-ssh-config Author: Yoichi Kawasaki Author-email: yoichi.kawasaki@outlook.com License: MIT Keywords: azure ssh config Platform: any Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Utilities Requires-Dist: azure-mgmt-compute (>=0.30.0rc6) Requires-Dist: azure-mgmt-network (>=0.30.0rc6) Requires-Dist: azure-mgmt-resource (>=0.30.0rc6) Requires-Dist: msrestazure Requires-Dist: simplejson UNKNOWN PK>HIŸØ‘ÈÈ%azuresshconfig-0.1.0.dist-info/RECORDazuresshconfig.py,sha256=Qw8GFCMbgeoAgCcxo63kuZxWqwExWl_a4miUQiAsbyc,12870 azuresshconfig-0.1.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 azuresshconfig-0.1.0.dist-info/METADATA,sha256=9K6JJ9z71w5_bNjw-IdGG9D3Be7vgQY5H5SrPLXwjmM,786 azuresshconfig-0.1.0.dist-info/RECORD,, azuresshconfig-0.1.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 azuresshconfig-0.1.0.dist-info/entry_points.txt,sha256=2M5L1ChqAnb4HZJFv_sMm58Cm4xWMpCQmbgV2qdwCYQ,54 azuresshconfig-0.1.0.dist-info/metadata.json,sha256=Jq_ZMV8c5EXRhDooNf96ySYKVIGGu9nOtahR4k_SFoI,1111 azuresshconfig-0.1.0.dist-info/top_level.txt,sha256=WfFQDDsyuV3L6OF7wml2ggkuA6lYHJILyGRsqcqoeUw,15 PK™5HIãkF2F2azuresshconfig.pyPK>HI^-Ò .u2azuresshconfig-0.1.0.dist-info/DESCRIPTION.rstPK>HI9®"ï66/Ë2azuresshconfig-0.1.0.dist-info/entry_points.txtPK>HI)³Æ†WW,N3azuresshconfig-0.1.0.dist-info/metadata.jsonPK>HIä å{,ï7azuresshconfig-0.1.0.dist-info/top_level.txtPK>HIŒ''\\$H8azuresshconfig-0.1.0.dist-info/WHEELPK>HI`Ôû('æ8azuresshconfig-0.1.0.dist-info/METADATAPK>HIŸØ‘ÈÈ%=<azuresshconfig-0.1.0.dist-info/RECORDPK¦H?