Skip to content

Commit

Permalink
Rewrite lxd_container to use pylxd (due to certificate validation issue
Browse files Browse the repository at this point in the history
  • Loading branch information
heino committed Jan 12, 2023
1 parent 4caa657 commit 8439e62
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 79 deletions.
82 changes: 82 additions & 0 deletions plugins/module_utils/lxd.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,85 @@ def default_key_file():

def default_cert_file():
return os.path.expanduser('~/.config/lxc/client.crt')







from pylxd import Client as PyLxdClient
from pylxd.exceptions import LXDAPIException, ClientConnectionFailed

import os

def pylxd_client(endpoint, client_cert=None, client_key=None, password=None, project=None, timeout=None, verify=True):
try:
# Connecting to the local unix socket
if endpoint is None or endpoint == '/var/lib/lxd/unix.socket' or endpoint == 'unix:/var/lib/lxd/unix.socket':
return PyLxdClient(
timeout=timeout,
project=project,
)

# Connecting to some other local socket
elif endpoint.startswith('/'):
return PyLxdClient(
endpoint=endpoint,
timeout=timeout,
project=project,
)

# Connecting to remote server
elif endpoint.startswith('https://'):

if client_cert is None:
client_cert = '~/.config/lxc/client.crt'
if client_key is None:
client_key = '~/.config/lxc/client.key'

# Expand an initial '~/'-path component
client_cert = os.path.expanduser(client_cert)
client_key = os.path.expanduser(client_key)

if not os.path.isfile(client_cert):
raise ValueError(
f"Invalid client_cert path: '{client_cert}' does not exist or is not a file."
)
if not os.path.isfile(client_key):
raise ValueError(
f"Invalid client_key path: '{client_key}' does not exist or is not a file."
)

client = PyLxdClient(
endpoint=endpoint,
cert=( client_cert, client_key ),
verify=verify,
timeout=timeout,
project=project,
)

if not client.trusted:
if password is None:
raise LXDClientException('The certificate is not yet trusted, but no trusted password provided')
try:
client.authenticate(password)
except LXDAPIException as e:
raise LXDClientException(str(e))

return client

# Invalid url
else:
raise ValueError('Invalid endpoint: ' + endpoint)

except ClientConnectionFailed as e:
raise LXDClientException(
f"Failed to connect to '{endpoint}' " + str(e) + ' !'
)
# TODO: Does this actually happen???
# except TypeError as e:
# # Happens when the verification failed.
# raise LXDClientException(
# f("Failed to connect to '{endpoint}' looks like the SSL verification failed, error was: {e}"
# )
142 changes: 63 additions & 79 deletions plugins/modules/lxd_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,10 @@
import time

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible_collections.community.general.plugins.module_utils.lxd import pylxd_client, LXDClientException
#from ansible.module_utils.lxd import pylxd_client, LXDClientException
from pylxd.exceptions import NotFound


# LXD_ANSIBLE_STATES is a map of states that contain values of methods used
# when a particular state is evoked.
Expand Down Expand Up @@ -451,22 +453,15 @@ def __init__(self, module):
self.addresses = None
self.target = self.module.params['target']
self.wait_for_container = self.module.params['wait_for_container']
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')

self.type = self.module.params['type']

# LXD Rest API provides additional endpoints for creating containers and virtual-machines.
self.api_endpoint = None
if self.type == 'container':
self.api_endpoint = '/1.0/containers'
elif self.type == 'virtual-machine':
self.api_endpoint = '/1.0/virtual-machines'

self.key_file = self.module.params.get('client_key')
if self.key_file is None:
self.key_file = '{0}/.config/lxc/client.key'.format(os.environ['HOME'])
self.cert_file = self.module.params.get('client_cert')
if self.cert_file is None:
self.cert_file = '{0}/.config/lxc/client.crt'.format(os.environ['HOME'])
self.client_key = self.module.params.get('client_key')
self.client_cert = self.module.params.get('client_cert')
self.trust_password = self.module.params.get('trust_password', None)
self.verify = self.module.params.get('verify')

self.debug = self.module._verbosity >= 4

try:
Expand All @@ -480,13 +475,18 @@ def __init__(self, module):
self.module.fail_json(msg=e.msg)

try:
self.client = LXDClient(
self.url, key_file=self.key_file, cert_file=self.cert_file,
debug=self.debug
self.client = pylxd_client(
endpoint=self.url,
client_cert=self.client_cert,
client_key=self.client_key,
password=self.trust_password,
project=self.project,
timeout=self.timeout,
verify=self.verify,
)
except LXDClientException as e:
self.module.fail_json(msg=e.msg)
self.trust_password = self.module.params.get('trust_password', None)

self.actions = []

def _build_config(self):
Expand All @@ -496,78 +496,48 @@ def _build_config(self):
if param_val is not None:
self.config[attr] = param_val

def _get_instance_json(self):
url = '{0}/{1}'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
return self.client.do('GET', url, ok_error_codes=[404])

def _get_instance_state_json(self):
url = '{0}/{1}/state'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
return self.client.do('GET', url, ok_error_codes=[404])

@staticmethod
def _instance_json_to_module_state(resp_json):
if resp_json['type'] == 'error':
return 'absent'
return ANSIBLE_LXD_STATES[resp_json['metadata']['status']]

def _change_state(self, action, force_stop=False):
url = '{0}/{1}/state'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
body_json = {'action': action, 'timeout': self.timeout}
if force_stop:
body_json['force'] = True
return self.client.do('PUT', url, body_json=body_json)

def _create_instance(self):
url = self.api_endpoint
url_params = dict()
if self.target:
url_params['target'] = self.target
if self.project:
url_params['project'] = self.project
if url_params:
url = '{0}?{1}'.format(url, urlencode(url_params))
config = self.config.copy()
config['name'] = self.name
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)

match self.type:
case 'container':
self.instance = self.client.containers.create(config=config, wait=True, target=self.target)
case 'virtual-machine':
self.instance = self.client.virtual_machines.create(config=config, wait=True, target=self.target)

# TODO: Re-add wait_for_container via stuff from Client.do() in pxd.py? Or just use/hijack the wait parameter above?

self.actions.append('create')

def _start_instance(self):
self._change_state('start')
self.instance.start(wait=True)
self.actions.append('start')

def _stop_instance(self):
self._change_state('stop', self.force_stop)
self.instance.stop(wait=True, force=self.force_stop)
self.actions.append('stop')

def _restart_instance(self):
self._change_state('restart', self.force_stop)
self.instance.restart(wait=True, force=self.force_stop)
self.actions.append('restart')

def _delete_instance(self):
url = '{0}/{1}'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
self.client.do('DELETE', url)
self.instance.delete(wait=True)
self.actions.append('delete')

def _freeze_instance(self):
self._change_state('freeze')
self.instance.freeze(wait=True)
self.actions.append('freeze')

def _unfreeze_instance(self):
self._change_state('unfreeze')
self.instance.unfreeze(wait=True)
self.actions.append('unfreez')

def _instance_ipv4_addresses(self, ignore_devices=None):
ignore_devices = ['lo'] if ignore_devices is None else ignore_devices

resp_json = self._get_instance_state_json()
resp_json = self.client.api.instances[self.name].state.get().json()
network = resp_json['metadata']['network'] or {}
network = dict((k, v) for k, v in network.items() if k not in ignore_devices) or {}
addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items()) or {}
Expand Down Expand Up @@ -706,22 +676,28 @@ def _apply_instance_configs(self):
if self._needs_to_change_instance_config('profiles'):
body_json['profiles'] = self.config['profiles']

url = '{0}/{1}'.format(self.api_endpoint, self.name)
if self.project:
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
self.client.do('PUT', url, body_json=body_json)
self.instance.api.put(json=body_json)
self.actions.append('apply_instance_configs')

def run(self):
"""Run the main method."""

try:
if self.trust_password is not None:
self.client.authenticate(self.trust_password)
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
try:
match self.type:
case 'container':
self.instance = self.client.containers.get(self.name)
case 'virtual-machine':
self.instance = self.client.virtual_machines.get(self.name)

self.old_instance_json = self.client.api.instances[self.name].get().json()

except NotFound:
self.instance = None
self.old_instance_json = { 'metadata': {} }

self.old_state = ANSIBLE_LXD_STATES[self.instance.status] if self.instance else 'absent'

self.old_instance_json = self._get_instance_json()
self.old_state = self._instance_json_to_module_state(self.old_instance_json)
action = getattr(self, LXD_ANSIBLE_STATES[self.state])
action()

Expand All @@ -732,20 +708,23 @@ def run(self):
'old_state': self.old_state,
'actions': self.actions
}
if self.client.debug:
result_json['logs'] = self.client.logs
# TODO: Is such logging needed at all when using the pylxd library
if self.debug:
# Provide a dummy log event
result_json['logs'] = [ '(Currently no client logging after switch to pylxd)' ]
if self.addresses is not None:
result_json['addresses'] = self.addresses
self.module.exit_json(**result_json)

except LXDClientException as e:
state_changed = len(self.actions) > 0
fail_params = {
'msg': e.msg,
'changed': state_changed,
'actions': self.actions
}
if self.client.debug:
fail_params['logs'] = e.kwargs['logs']
if self.debug:
fail_params['logs'] = e.kwargs['logs']
self.module.fail_json(**fail_params)


Expand Down Expand Up @@ -814,7 +793,8 @@ def main():
),
url=dict(
type='str',
default=ANSIBLE_LXD_DEFAULT_URL
default=ANSIBLE_LXD_DEFAULT_URL,
aliasses=['endpoint']
),
snap_url=dict(
type='str',
Expand All @@ -828,6 +808,10 @@ def main():
type='path',
aliases=['cert_file']
),
verify=dict(
type='bool',
default=True
),
trust_password=dict(type='str', no_log=True)
),
supports_check_mode=False,
Expand Down

0 comments on commit 8439e62

Please sign in to comment.