diff options
-rw-r--r-- | cloudinit/config/cc_vyos.py | 185 |
1 files changed, 89 insertions, 96 deletions
diff --git a/cloudinit/config/cc_vyos.py b/cloudinit/config/cc_vyos.py index 37f45cf8..2c40fb8d 100644 --- a/cloudinit/config/cc_vyos.py +++ b/cloudinit/config/cc_vyos.py @@ -20,14 +20,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os import re import ipaddress -from cloudinit import stages -from cloudinit import util +from os import path +from uuid import uuid4 +from cloudinit import log as logging +from cloudinit.ssh_util import AuthKeyLineParser from cloudinit.distros import ug_util from cloudinit.settings import PER_INSTANCE -from cloudinit import log as logging from vyos.configtree import ConfigTree # configure logging @@ -45,57 +45,44 @@ class VyosError(Exception): # configure user account with password -def set_pass_login(config, user, password, encrypted_pass): +def set_pass_login(config, user, password): + # check if a password string is a hash or a plaintext password + # the regex from Cloud-init documentation, so we should trust it for this purpose + encrypted_pass = re.match(r'\$(1|2a|2y|5|6)(\$.+){2}', password) if encrypted_pass: logger.debug("Configuring encrypted password for: {}".format(user)) config.set(['system', 'login', 'user', user, 'authentication', 'encrypted-password'], value=password, replace=True) else: - logger.debug("Configuring clear-text password for: {}".format(user)) + logger.debug("Configuring plaintext password password for: {}".format(user)) config.set(['system', 'login', 'user', user, 'authentication', 'plaintext-password'], value=password, replace=True) config.set_tag(['system', 'login', 'user']) # configure user account with ssh key -def set_ssh_login(config, user, key_string, ssh_key_number): - key_type = None - key_data = None - key_name = None +def set_ssh_login(config, user, key_string): + ssh_parser = AuthKeyLineParser() + key_parsed = ssh_parser.parse(key_string) + logger.debug("Parsed SSH public key: type: {}, base64: \"{}\", comment: {}, options: {}".format(key_parsed.keytype, key_parsed.base64, key_parsed.comment, key_parsed.options)) - if key_string == '': - logger.error("No keys found.") + if key_parsed.keytype not in ['ssh-dss', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ssh-ed25519', 'ecdsa-sha2-nistp521']: + logger.error("Key type {} not supported.".format(key_parsed.keytype)) return - key_parts = key_string.split(None) - - for key in key_parts: - if 'ssh-dss' in key or 'ssh-rsa' in key: - key_type = key - - if key.startswith('AAAAB3NzaC1yc2E') or key.startswith('AAAAB3NzaC1kc3M'): - key_data = key - - if not key_type: - logger.error("Key type not defined, wrong ssh key format.") - return - - if not key_data: + if not key_parsed.base64: logger.error("Key base64 not defined, wrong ssh key format.") return - if len(key_parts) > 2: - if key_parts[2] != key_type or key_parts[2] != key_data: - key_name = key_parts[2] - else: - key_name = "cloud-init-%s" % ssh_key_number - else: - key_name = "cloud-init-%s" % ssh_key_number + if not key_parsed.comment: + key_parsed.comment = "cloud-init-{}".format(uuid4()) - logger.debug("Configuring SSH {} public key for: {}".format(key_type, user)) - config.set(['system', 'login', 'user', user, 'authentication', 'public-keys', key_name, 'key'], value=key_data, replace=True) - config.set(['system', 'login', 'user', user, 'authentication', 'public-keys', key_name, 'type'], value=key_type, replace=True) + config.set(['system', 'login', 'user', user, 'authentication', 'public-keys', key_parsed.comment, 'key'], value=key_parsed.base64, replace=True) + config.set(['system', 'login', 'user', user, 'authentication', 'public-keys', key_parsed.comment, 'type'], value=key_parsed.keytype, replace=True) + if key_parsed.options: + config.set(['system', 'login', 'user', user, 'authentication', 'public-keys', key_parsed.comment, 'options'], value=key_parsed.options, replace=True) config.set_tag(['system', 'login', 'user']) config.set_tag(['system', 'login', 'user', user, 'authentication', 'public-keys']) + logger.debug("Configured SSH public key for user: {}".format(user)) # filter hostname to be sure that it can be applied @@ -320,14 +307,14 @@ def set_config_interfaces_v1(config, iface_config): config.set_tag(['protocols', 'static', 'route']) config.set_tag(['protocols', 'static', 'route', ip_network.compressed, 'next-hop']) if 'metric' in iface_config: - config.set(['protocols', 'static', 'route', ip_network.compressed, 'next-hop', iface_config['gateway'], distance], value=iface_config['metric'], replace=True) + config.set(['protocols', 'static', 'route', ip_network.compressed, 'next-hop', iface_config['gateway'], 'distance'], value=iface_config['metric'], replace=True) if ip_network.version == 6: logger.debug("Configuring IPv6 route: {} via {}".format(ip_network.compressed, iface_config['gateway'])) config.set(['protocols', 'static', 'route6', ip_network.compressed, 'next-hop'], value=iface_config['gateway'], replace=True) config.set_tag(['protocols', 'static', 'route6']) config.set_tag(['protocols', 'static', 'route6', ip_network.compressed, 'next-hop']) if 'metric' in iface_config: - config.set(['protocols', 'static', 'route6', ip_network.compressed, 'next-hop', iface_config['gateway'], distance], value=iface_config['metric'], replace=True) + config.set(['protocols', 'static', 'route6', ip_network.compressed, 'next-hop', iface_config['gateway'], 'distance'], value=iface_config['metric'], replace=True) except Exception as err: logger.error("Impossible to detect IP protocol version: {}".format(err)) @@ -421,19 +408,41 @@ def set_config_hostname(config, hostname): # main config handler def handle(name, cfg, cloud, log, _args): - init = stages.Init() - dc = init.fetch() + logger.debug("Cloud-init config: {}".format(cfg)) + # fetch all required data from Cloud-init + # Datasource name + dsname = cloud.datasource.dsname + logger.debug("Datasource: {}".format(dsname)) + # Metadata (datasource specific) + metadata_ds = cloud.datasource.metadata + logger.debug("Meta-Data ds: {}".format(metadata_ds)) + # Metadata in stable v1 format (the same structure for all datasources) + metadata_v1 = cloud.datasource._get_standardized_metadata().get('v1') + logger.debug("Meta-Data v1: {}".format(metadata_v1)) + # User-Data + userdata = cloud.datasource.userdata + logger.debug("User-Data: {}".format(userdata)) + # Vendor-Data + vendordata = cloud.datasource.vendordata + logger.debug("Vendor-Data: {}".format(vendordata)) + # Network-config + netcfg = cloud.datasource.network_config + logger.debug("Network-config: {}".format(netcfg)) + # Hostname with domain (if exist) + hostname = cloud.get_hostname(fqdn=True, metadata_only=True) + logger.debug("Hostname: {}".format(hostname)) + # Get users list + (users, _) = ug_util.normalize_users_groups(cfg, cloud.distro) + logger.debug("Users: {}".format(users)) + (default_user, default_user_config) = ug_util.extract_default(users) + logger.debug("Default user: {}".format(default_user)) + + # VyOS configuration file selection cfg_file_name = '/opt/vyatta/etc/config/config.boot' bak_file_name = '/opt/vyatta/etc/config.boot.default' - metadata = cloud.datasource.metadata - (netcfg, _) = init._find_networking_config() - (users, _) = ug_util.normalize_users_groups(cfg, cloud.distro) - (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud, metadata_only=True) - ssh_key_number = 1 - network_configured = False # open configuration file - if not os.path.exists(cfg_file_name): + if not path.exists(cfg_file_name): file_name = bak_file_name else: file_name = cfg_file_name @@ -443,56 +452,42 @@ def handle(name, cfg, cloud, log, _args): config_file = f.read() config = ConfigTree(config_file) - # configure system logins - if 'Azure' in dc.dsname: - logger.debug("Detected Azure environment") - encrypted_pass = True - for key, val in users.items(): - user = key - - if 'passwd' in val: - password = val.get('passwd') - set_pass_login(config, user, password, encrypted_pass) - - vyos_keys = metadata['public-keys'] + # Initialization of variables + network_configured = False - for ssh_key in vyos_keys: - set_ssh_login(config, user, ssh_key, ssh_key_number) - ssh_key_number = ssh_key_number + 1 - else: - encrypted_pass = False - for user in users: - password = util.get_cfg_option_str(cfg, 'passwd', None) - - if not password: - password = util.get_cfg_option_str(cfg, 'password', None) - - if password and password != '': - hash = re.match(r"(^\$.\$)", password) - hash_count = password.count('$') - if hash and hash_count >= 3: - base64 = password.split('$')[3] - base_64_len = len(base64) - if ((hash.group(1) == '$1$' and base_64_len == 22) or - (hash.group(1) == '$5$' and base_64_len == 43) or - (hash.group(1) == '$6$' and base_64_len == 86)): - encrypted_pass = True - set_pass_login(config, user, password, encrypted_pass) - - vyos_keys = cloud.get_public_ssh_keys() or [] - if 'ssh_authorized_keys' in cfg: - cfgkeys = cfg['ssh_authorized_keys'] - vyos_keys.extend(cfgkeys) - - for ssh_key in vyos_keys: - set_ssh_login(config, user, ssh_key, ssh_key_number) - ssh_key_number = ssh_key_number + 1 + # configure system logins + # Prepare SSH public keys for default user, to be sure that global keys applied to the default account (if it exist) + ssh_keys = metadata_v1['public_ssh_keys'] + # append SSH keys from cloud-config + ssh_keys.extend(cfg.get('ssh_authorized_keys', [])) + # Configure authentication for default user account + if default_user: + # key-based + for ssh_key in ssh_keys: + set_ssh_login(config, default_user, ssh_key) + # password-based + password = cfg.get('password') + if password: + set_pass_login(config, default_user, password) + + # Configure all users accounts + for user, user_cfg in users.items(): + # Configure password-based authentication + password = user_cfg.get('passwd') + if password and password != '': + set_pass_login(config, user, password) + + # Configure key-based authentication + for ssh_key in user_cfg.get('ssh_authorized_keys', []): + set_ssh_login(config, user, ssh_key) # apply settings from OVF template - if 'OVF' in dc.dsname: - set_config_ovf(config, metadata) + if 'OVF' in dsname: + set_config_ovf(config, metadata_ds) + # Empty hostname option may be interpreted as 'null' string by some hypervisors + # we need to replace it to the empty value to process it later properly if hostname and hostname == 'null': - hostname = 'vyos' + hostname = None network_configured = True # process networking configuration data @@ -518,9 +513,7 @@ def handle(name, cfg, cloud, log, _args): # enable SSH service set_config_ssh(config) # configure hostname - if fqdn: - set_config_hostname(config, fqdn) - elif hostname: + if hostname: set_config_hostname(config, hostname) else: set_config_hostname(config, 'vyos') |